zl程序教程

您现在的位置是:首页 >  系统

当前栏目

linux进程的管理与调度 --- 基础知识

Linux基础知识进程 管理 --- 调度
2023-09-11 14:19:53 时间

 进程的管理与调度是所有操作系统的核心功能。从内核的角度来看,进程是内核分配资源(CPU,Memory)的重要单元,是计算机用来管理这些资源的一种抽象。

进程状态

1、TASK_RUNNING

表示进程要么正在执行,要么准备执行,等待cpu时间片的调度

2、TASK_INTERRUPTIBLE

表示进程被挂起(睡眠),直到某个条件成立触发CPU中断或者发送信号唤醒该进程,将其状态改成TASK_RUNNING,比如某个TASK_RUNNING的进程需要读取文件,发起系统调用从用户态进入内核态,内核将其状态改成TASK_INTERRUPTIBLE,然后调用磁盘驱动程序读取文件,CPU执行其他任务;待磁盘读取文件完毕,磁盘发送CPU中断信号,CPU将读取的文件内容传给进程,进程由内核态切换到用户态,处理文件内容。一般情况下,进程列表中的绝大多数进程都处于TASK_INTERRUPTIBLE状态,除非机器的负载很高。

3、TASK_UNINTERRUPTIBLE

与TASK_INTERRUPTIBLE类似,区别是不能被外部信号唤醒,只能通过CPU中断唤醒。该状态总是非常短暂的,通过ps命令基本上不可能捕捉,主要用于避免内核某些处理过程被中断,如进程与设备交互的过程,中断会造成设备陷入不可控的状态。

4、TASK_STOPPED

表示进程的执行已停止,向进程发送一个SIGSTOP,SIGTSTP,SIGTTIN,SIGTTOU信号,它就会因响应该信号而进入TASK_STOPPED状态,向进程发送一个向进程SIGCONT信号,可以让其恢复到TASK_RUNNING状态。

5、TASK_TRACED

表示进程的执行已停止,等待跟踪它的进程对它进行操作,比如在gdb中对被跟踪的进程下一个断点,进程在断点处停下来的时候就处于TASK_TRACED状态。处于TASK_TRACED状态的进程不能响应SIGCONT信号而被唤醒,只能等到调试进程通过ptrace系统调用执行PTRACE_CONT、PTRACE_DETACH等操作或调试进程退出,被调试的进程才能恢复TASK_RUNNING状态。

6、EXIT_ZOMBIE

表示进程已终止,正等待其父进程执行wait类系统调用收集关于它的一些统计信息如退出码,内核此时无法回收该进程的进程描述符。如果父进程未执行wait类系统调用并退出了,子进程会转交给上一级的父进程,直到最终的init进程,由上一级父进程执行wait类系统调用。

7、EXIT_DEAD

表示进程已终止,父进程已经执行wait类系统调用,进程即将被内核删除,该状态非常短暂。

Linux Kernel 2.6.25 引入了一种新的进程睡眠状态,TASK_KILLABLE:当进程处于这种可以终止的新睡眠状态中,它的运行原理类似于 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。

 

用户态 / 内核态切换

内核态:

当一个任务(进程)执行系统调用而陷入内核代码中执行时,我们就称进程处于内核运行态。此时处理器处于特权级最高的(0级)内核代码中执行。当进程处于内核态时,执行的内核代码会使用当前进程的内核栈

用户态:

每个进程都有自己的内核栈。当进程在执行用户自己的代码时,则称其处于用户运行态。即此时处理器在特权级最低的(3级)用户代码中运行。

从用户态到内核态的转换发生在:

  1.  发生系统调用
  2. CPU执行异常
  3. 外围设备发来中断请求

进程是资源分配的基本单位, 因此进程之间切换时,需要保存、装载各种状态数据等资源, 所需的代价较高。线程是操作系统调度的基本单位,同一个进程内的线程共享操作系统给该进程分配的资源,共享一个地址空间,不需要特别麻烦的切换页表、刷新TLB,只需要将寄存器刷新一遍就行,比进程切换开销少。

调度是在内核态运行的,用户态切换到内核态通过中断完成,操作系统切换线程上下文的步骤如下所示:

1)保留用户态现场(上下文、寄存器、用户栈等)
2)复制用户态参数,用户栈切到内核栈,进入内核态
3)代码安全检查(内核不信任用户态代码)
4)执行内核态代码,如切换线程
5)复制内核态代码执行结果,回到用户态
6)恢复用户态现场(上下文、寄存器、用户栈等)

 

协程

协程是一种用户态线程,它比线程更加轻量并且协程对于操作系统是并不可见的,同一时刻一个CPU只会执行一个协程。

每个线程都有自己的TCB都有自己的堆栈,创建一个线程是有很大的花销的

进程/线程创建

进程创建

进程通过 fork 和 exec 调用来进行创建。

fork 通过拷贝当前进程创建一个子进程,区别在于 PID

exec 负责读取可执行文件,并载入地址空间开始运行

对于 fork 这种方式,Linux 出于效率的考虑,并没有在实现的时候直接复制所有信息, 而是使用了一种叫做写时拷贝(copy-on-write,COW)。内核并不复制整个进程地址空间,只有在需要写入的时候,数据才会复制。

fork 是通过 clone() 系统调用来实现的,然后 clone 又去调用 do_fork (kernel/fork.c)

在 do_fork 中调用 copy_process 函数创建一个新的 task_struct 描述符

fork() --> clone() --> do_fork() --> copy_process() --> dup_task_struct()

 

线程创建

线程其实和进程共享了一些资源,内核其实把线程当成是一个进程来对待(包括调度,也用 task_struct 来描述),内核看起来,就像是普通的进程一样(一些标志不一样)。

创建线程的时候,和创建进程类似,只不过在调用 clone 的时候,传入了一些参数标志:

clone(CLONE_VM | CLONE_FS | CLONE_FILES | CLONE_SIGHAND,0);

这个和普通的 差不多,只是共享了地址空间、文件系统资源、文件描述符、信号处理程序。

内核线程创建

内核进程需要在后台执行一些操作,这种任务可以通过内核线程(kernel thread)完成。它与普通的线程的区别在于,没有独立地址空间。也就是 task_struct 的 mm 指针为 NULL,他们只在内核空间运行,能被调度和抢占。比如,flush,ksoftirqd等等。

对于内核线程没有自己的独立地址空间的这个说法,具体的理解为:

用户空间:不同进程的线性地址操作虽然仍是统一的,但物理地址却因为独立地址空间的缘故而映射不一致,乃至于影响不到其他进程的资源。独立的地址空间意味着数据修改的彼此独立性,即严防不同进程之间干扰。这符合“进程是系统资源分配的最小单位”的要求
内核空间,所有线程虚拟地址对应的物理地址都是一样的, 所以说是共享。
打个比方:

A,B为用户进程,C为内核线程。那么,A,B进程的0-3G的线性地址空间是相互独立的,而 C 并独立属于自己的地址空间,它的地址空间在 3G~4G 的地方,这部分是 A,B,C都共享的,所以 C 不能称之为有独立的地址空间。

内核线程通过函数 kthread_create 来创建:

#define kthread_create(threadfn, data, namefmt, arg...) \
    kthread_create_on_node(threadfn, data, NUMA_NO_NODE, namefmt, ##arg)

struct task_struct *kthread_create_on_node(int (*threadfn)(void *data),
                       void *data, int node,
                       const char namefmt[],
                       ...)
{
    struct task_struct *task;
    va_list args;

    va_start(args, namefmt);
    task = __kthread_create_on_node(threadfn, data, node, namefmt, args);
    va_end(args);

    return task;
}

其中 threadfn 是内核线程的执行函数,data 是传递给 threadfn 的入参,node 是内存节点(NUMA 体系),namefmt 是线程名称。通过这个 API 创建的内核线程不会马上工作,出于一个待唤醒的状态,如果希望其工作,还需调用 wake_up_process 唤醒它才行。

这里内核实现了一个函数 kthread_run 来执行上述两条命令,即调用了这个 kthread_run 函数,内核线程便会立马运行了,它的实现也就是顺序 call 了 kthread_create 和 wake_up_process 。

使用 kthread_stop 来退出。