在 Linux 里面,无论是进程,还是线程,到了内核里面,我们统一都叫任务(Task),由一个统一的结构 task_struct 进行管理。75c4d28a9d2daa4acc1107832be84e2d.webp
接下来就看看 Linux 的任务管理都应该干些啥?

首先, Linux 内核弄了一个链表,将所有的 task_struct 串起来。

  1. struct list_head tasks;

任务 ID

每一个任务都应该有一个 ID,作为这个任务的唯一标识。到时候排期啊、下发任务啊等等,都按 ID 来,就不会产生歧义。task_struct 里面涉及任务 ID 的,有下面几个:

  1. pid_t pid; //process id
  2. pid_t tgid; //thread group id
  3. struct task_struct *group_leader;

进程和线程到了内核这里,统一变成了任务,这就带来两个问题。

第一个问题是,任务展示。前面我们学习命令行的时候,知道 ps 命令可以展示出所有的进程。但是如果你是这个命令的实现者,到了内核,按照上面的任务列表把这些命令都显示出来,把所有的线程全都平摊开来显示给用户。用户肯定觉得既复杂又困惑。复杂在于,列表这么长;困惑在于,里面出现了很多并不是自己创建的线程。

第二个问题是,给任务下发指令。前面我们学习命令行的时候,知道可以通过 kill 来给进程发信号,通知进程退出。如果发给了其中一个线程,我们就不能只退出这个线程,而是应该退出整个进程。当然,有时候,我们希望只给某个线程发信号。

所以在内核中,它们虽然都是任务,但是应该加以区分。

任何一个进程,如果只有主线程,那 pid 是自己,tgid 是自己,group_leader 指向的还是自己。但是,如果一个进程创建了其他线程,那就会有所变化了。线程有自己的 pid,tgid 就是进程的主线程的 pid,group_leader 指向的就是进程的主线程。id 用于辨别身份,group_leader 指针指向地址。

信号处理

  1. /* Signal handlers: */
  2. struct signal_struct *signal;
  3. struct sighand_struct *sighand;
  4. sigset_t blocked;
  5. sigset_t real_blocked;
  6. sigset_t saved_sigmask;
  7. struct sigpending pending;
  8. unsigned long sas_ss_sp;
  9. size_t sas_ss_size;
  10. unsigned int sas_ss_flags;

这里定义了哪些信号被阻塞暂不处理(blocked),哪些信号尚等待处理(pending),哪些信号正在通过信号处理函数进行处理(sighand)。处理的结果可以是忽略,可以是结束进程等等。

信号处理函数默认使用用户态的函数栈,当然也可以开辟新的栈专门用于信号处理,这就是 sas_ss_xxx 这三个变量的作用。

任务状态

e0019fcd11ff1ba33a3389e285b6a121.webp

在 task_struct 里面,涉及任务状态的是下面这几个变量:

  1. volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
  2. int exit_state;
  3. unsigned int flags;

state(状态)可以取的值定义在 include/linux/sched.h 头文件中。

  1. /* Used in tsk->state: */
  2. #define TASK_RUNNING 0
  3. #define TASK_INTERRUPTIBLE 1
  4. #define TASK_UNINTERRUPTIBLE 2
  5. #define __TASK_STOPPED 4
  6. #define __TASK_TRACED 8
  7. /* Used in tsk->exit_state: */
  8. #define EXIT_DEAD 16
  9. #define EXIT_ZOMBIE 32
  10. #define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
  11. /* Used in tsk->state again: */
  12. #define TASK_DEAD 64
  13. #define TASK_WAKEKILL 128
  14. #define TASK_WAKING 256
  15. #define TASK_PARKED 512
  16. #define TASK_NOLOAD 1024
  17. #define TASK_NEW 2048
  18. #define TASK_STATE_MAX 4096

从定义的数值很容易看出来,state 是通过 bitset 的方式设置的,也就是说,当前是什么状态,哪一位就置一。e2fa348c67ce41ef730048ff9ca4c988.webp
TASK_RUNNING 并不是说进程正在运行,而是表示进程在时刻准备运行的状态。当处于这个状态的进程获得时间片的时候,就是在运行中;如果没有获得时间片,就说明它被其他进程抢占了,在等待再次分配时间片。

在运行中的进程,一旦要进行一些 I/O 操作,需要等待 I/O 完毕,这个时候会释放 CPU,进入睡眠状态。在 Linux 中,有三种睡眠状态。

第一种是 TASK_INTERRUPTIBLE,可中断的睡眠状态。这是一种浅睡眠的状态,也就是说,虽然在睡眠,等待 I/O 完成,但是这个时候一个信号来的时候,进程还是要被唤醒。只不过唤醒后,不是继续刚才的操作,而是进行信号处理。当然程序员可以根据自己的意愿,来写信号处理函数,例如收到某些信号,就放弃等待这个 I/O 操作完成,直接退出;或者收到某些信息,继续等待。

第二种是 TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。这是一种深度睡眠状态,不可被信号唤醒,只能死等 I/O 操作完成。一旦 I/O 操作因为特殊原因不能完成,这个时候,谁也叫不醒这个进程了。你可能会说,我 kill 它呢?别忘了,kill 本身也是一个信号,既然这个状态不可被信号唤醒,kill 信号也被忽略了。除非重启电脑,没有其他办法。

第三种是 TASK_KILLABLE,可以终止的新睡眠状态。进程处于这种状态中,它的运行原理类似 TASK_UNINTERRUPTIBLE,只不过可以响应致命信号。从定义可以看出,TASK_WAKEKILL 用于在接收到致命信号时唤醒进程,而 TASK_KILLABLE 相当于这两位都设置了。

  1. #define TASK_KILLABLE (TASK_WAKEKILL | TASK_UNINTERRUPTIBLE)

TASK_STOPPED 是在进程接收到 SIGSTOP、SIGTTIN、SIGTSTP 或者 SIGTTOU 信号之后进入该状态。

TASK_TRACED 表示进程被 debugger 等进程监视,进程执行被调试程序所停止。当一个进程被另外的进程所监视,每一个信号都会让进程进入该状态。

一旦一个进程要结束,先进入的是 EXIT_ZOMBIE 状态,但是这个时候它的父进程还没有使用 wait() 等系统调用来获知它的终止信息,此时进程就成了僵尸进程。该进程会继续停留在系统的进程表中,占用内核资源。

EXIT_DEAD 是进程的最终状态。EXIT_ZOMBIE 和 EXIT_DEAD 也可以用于 exit_state。

上面的进程状态和进程的运行、调度有关系,还有其他的一些状态,我们称为标志。放在 flags 字段中,这些字段都被定义成为,以 PF 开头。

  1. #define PF_EXITING 0x00000004
  2. #define PF_VCPU 0x00000010
  3. #define PF_FORKNOEXEC 0x00000040
  1. <br />**PF_EXITING** 表示正在退出。当有这个 flag 的时候,在函数 `find_alive_thread` 中,找活着的线程,遇到有这个 flag 的,就直接跳过。

PF_VCPU 表示进程运行在虚拟 CPU 上。在函数 account_system_time中,统计进程的系统运行时间,如果有这个 flag,就调用 account_guest_time,按照客户机的时间进行统计。

PF_FORKNOEXEC 表示 fork 完了,还没有 exec。在 _do_fork 函数里面调用 copy_process,这个时候把 flag 设置为 PF_FORKNOEXEC。当 exec 中调用了 load_elf_binary 的时候,又把这个 flag 去掉。

进程调度

进程的状态切换往往涉及调度,下面这些字段都是用于调度的。为了理解 task_struct 进程管理的全貌,先在这里列一下。

//是否在运行队列上
int        on_rq;
//优先级
int        prio;
int        static_prio;
int        normal_prio;
unsigned int      rt_priority;
//调度器类
const struct sched_class  *sched_class;
//调度实体
struct sched_entity    se;
struct sched_rt_entity    rt;
struct sched_dl_entity    dl;
//调度策略
unsigned int      policy;
//可以使用哪些CPU
int        nr_cpus_allowed;
cpumask_t      cpus_allowed;
struct sched_info    sched_info;

总结延伸

016ae7fb63f8b3fd0ca072cb9964e3e8.webp

  • 内核中进程, 线程统一为任务, 由 taks_struct 表示
  • 通过链表串起 task_struct
  • task_struct 中包含: 任务ID; 任务状态; 信号处理相关字段; 调度相关字段; 亲缘关系; 权限相关; 运行统计; 内存管理; 文件与文件系统; 内核栈;
  • 任务 ID; 包含 pid, tgid 和 *group_leader
    • pid(process id, 线程的id); tgid(thread group id, 所属进程[主线程]的id); group_leader 指向 tgid 的结构体
    • 通过对比 pid 和 tgid 可判断是进程还是线程
  • 信号处理, 包含阻塞暂不处理; 等待处理; 正在处理的信号
    • 信号处理函数默认使用用户态的函数栈, 也可以开辟新的栈专门用于信号处理, 由 sas_ss_xxx 指定
    • 通过 pending/shared_pending 区分进程和线程的信号
  • 任务状态; 包含 state; exit_state; flags
    • 准备运行状态 TASK_RUNNING
    • 睡眠状态:可中断; 不可中断; 可杀
      • 可中断 TASK_INTERRUPTIBLE, 收到信号要被唤醒
      • 不可中断 TASK_UNINTERRUPTIBLE, 收到信号不会被唤醒, 不能被kill, 只能重启
      • 可杀 TASK_KILLABLE, 可以响应致命信号, 由不可中断与 TASK_WAKEKILL 组合
    • 停止状态 TASK_STOPPED, 由信号 SIGSTOP, SIGTTIN, SIGTSTP 与 SIGTTOU 触发进入
    • 调试跟踪 TASK_TRACED, 被 debugger 等进程监视时进入
    • 结束状态(包含 exit_state)
      • EXIT_ZOMBIE, 父进程还没有 wait()
      • EXIT_DEAD, 最终状态
    • flags, 例如 PF_VCPU 表示运行在虚拟 CPU 上; PF_FORKNOEXEC _do_fork 函数里设置, exec 函数中清除
  • 进程调度; 包含 是否在运行队列; 优先级; 调度策略; 可以使用那些 CPU 等信息.