You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

220 lines
12 KiB
Markdown

2 years ago
# 12 | 进程数据结构(上):项目多了就需要项目管理系统
前面两节,我们讲了如何使用系统调用,创建进程和线程。你是不是觉得进程和线程管理,还挺复杂的呢?如此复杂的体系,在内核里面应该如何管理呢?
有的进程只有一个线程有的进程有多个线程它们都需要由内核分配CPU来干活。可是CPU总共就这么几个应该怎么管理怎么调度呢你是老板这个事儿得你来操心。
首先,我们得明确,公司的项目售前售后人员,接来了这么多的项目,这是个好事儿。这些项目都通过办事大厅立了项的,有的需要整个项目组一起开发,有的是一个项目组分成多个小组并行开发。无论哪种模式,到你这个老板这里,都需要有一个项目管理体系,进行统一排期、统一管理和统一协调。这样,你才能对公司的业务了如指掌。
那具体应该怎么做呢还记得咱们平时开发的时候用的项目管理软件Jira吧它的办法对我们来讲就很有参考意义。
我们这么来看,其实,无论是一个大的项目组一起完成一个大的功能(单体应用模式),还是把一个大的功能拆成小的功能并行开发(微服务模式),这些都是开发组根据客户的需求来定的,项目经理没办法决定,但是从项目经理的角度来看,这些都是任务,需要同样关注进度、协调资源等等。
同样在Linux里面无论是进程还是线程到了内核里面我们统一都叫任务Task由一个统一的结构**task\_struct**进行管理。这个结构非常复杂,但你也不用怕,我们慢慢来解析。
![](https://static001.geekbang.org/resource/image/75/2d/75c4d28a9d2daa4acc1107832be84e2d.jpeg)
接下来,我们沿着建立项目管理体系的思路,设想一下,**Linux的任务管理都应该干些啥**
首先所有执行的项目应该有个项目列表吧所以Linux内核也应该先弄一个**链表**将所有的task\_struct串起来。
```
struct list_head tasks;
```
接下来,我们来看每一个任务都应该包含哪些字段。
## 任务ID
每一个任务都应该有一个ID作为这个任务的唯一标识。到时候排期啊、下发任务啊等等都按ID来就不会产生歧义。
task\_struct里面涉及任务ID的有下面几个
```
pid_t pid;
pid_t tgid;
struct task_struct *group_leader;
```
你可能觉得奇怪既然是ID有一个就足以做唯一标识了这个怎么看起来这么麻烦这是因为上面的进程和线程到了内核这里统一变成了任务这就带来两个问题。
第一个问题是,**任务展示**。
啥是任务展示呢?这么说吧,你作为老板,想了解的肯定是,公司都接了哪些项目,每个项目多少营收。什么项目执行是不是分了小组,每个小组是啥情况,这些细节,项目经理没必要全都展示给你看。
前面我们学习命令行的时候知道ps命令可以展示出所有的进程。但是如果你是这个命令的实现者到了内核按照上面的任务列表把这些命令都显示出来把所有的线程全都平摊开来显示给用户。用户肯定觉得既复杂又困惑。复杂在于列表这么长困惑在于里面出现了很多并不是自己创建的线程。
第二个问题是,**给任务下发指令**。
如果客户突然给项目组提个新的需求,比如说,有的客户觉得项目已经完成,可以终止;再比如说,有的客户觉得项目做到一半没必要再进行下去了,可以中止,这时候应该给谁发指令?当然应该给整个项目组,而不是某个小组。我们不能让客户看到,不同的小组口径不一致。这就好比说,中止项目的指令到达一个小组,这个小组很开心就去休息了,同一个项目组的其他小组还干的热火朝天的。
Linux也一样前面我们学习命令行的时候知道可以通过kill来给进程发信号通知进程退出。如果发给了其中一个线程我们就不能只退出这个线程而是应该退出整个进程。当然有时候我们希望只给某个线程发信号。
所以在内核中它们虽然都是任务但是应该加以区分。其中pid是process idtgid是thread group ID。
任何一个进程如果只有主线程那pid是自己tgid是自己group\_leader指向的还是自己。
但是如果一个进程创建了其他线程那就会有所变化了。线程有自己的pidtgid就是进程的主线程的pidgroup\_leader指向的就是进程的主线程。
好了有了tgid我们就知道tast\_struct代表的是一个进程还是代表一个线程了。
## 信号处理
这里既然提到了下发指令的问题我就顺便提一下task\_struct里面关于信号处理的字段。
```
/* Signal handlers: */
struct signal_struct *signal;
struct sighand_struct *sighand;
sigset_t blocked;
sigset_t real_blocked;
sigset_t saved_sigmask;
struct sigpending pending;
unsigned long sas_ss_sp;
size_t sas_ss_size;
unsigned int sas_ss_flags;
```
这里定义了哪些信号被阻塞暂不处理blocked哪些信号尚等待处理pending哪些信号正在通过信号处理函数进行处理sighand。处理的结果可以是忽略可以是结束进程等等。
信号处理函数默认使用用户态的函数栈当然也可以开辟新的栈专门用于信号处理这就是sas\_ss\_xxx这三个变量的作用。
上面我说了下发信号的时候,需要区分进程和线程。从这里我们其实也能看出一些端倪。
task\_struct里面有一个struct sigpending pending。如果我们进入struct signal\_struct \*signal去看的话还有一个struct sigpending shared\_pending。它们一个是本任务的一个是线程组共享的。
关于信号,你暂时了解到这里就够用了,后面我们会有单独的章节进行解读。
## 任务状态
作为一个项目经理另外一个需要关注的是项目当前的状态。例如在Jira里面任务的运行就可以分成下面的状态。
![](https://static001.geekbang.org/resource/image/e0/21/e0019fcd11ff1ba33a3389e285b6a121.jpg)
在task\_struct里面涉及任务状态的是下面这几个变量
```
volatile long state; /* -1 unrunnable, 0 runnable, >0 stopped */
int exit_state;
unsigned int flags;
```
state状态可以取的值定义在include/linux/sched.h头文件中。
```
/* Used in tsk->state: */
#define TASK_RUNNING 0
#define TASK_INTERRUPTIBLE 1
#define TASK_UNINTERRUPTIBLE 2
#define __TASK_STOPPED 4
#define __TASK_TRACED 8
/* Used in tsk->exit_state: */
#define EXIT_DEAD 16
#define EXIT_ZOMBIE 32
#define EXIT_TRACE (EXIT_ZOMBIE | EXIT_DEAD)
/* Used in tsk->state again: */
#define TASK_DEAD 64
#define TASK_WAKEKILL 128
#define TASK_WAKING 256
#define TASK_PARKED 512
#define TASK_NOLOAD 1024
#define TASK_NEW 2048
#define TASK_STATE_MAX 4096
```
从定义的数值很容易看出来state是通过bitset的方式设置的也就是说当前是什么状态哪一位就置一。
![](https://static001.geekbang.org/resource/image/e2/88/e2fa348c67ce41ef730048ff9ca4c988.jpeg)
TASK\_RUNNING并不是说进程正在运行而是表示进程在时刻准备运行的状态。当处于这个状态的进程获得时间片的时候就是在运行中如果没有获得时间片就说明它被其他进程抢占了在等待再次分配时间片。
在运行中的进程一旦要进行一些I/O操作需要等待I/O完毕这个时候会释放CPU进入睡眠状态。
在Linux中有两种睡眠状态。
一种是**TASK\_INTERRUPTIBLE****可中断的睡眠状态**。这是一种浅睡眠的状态也就是说虽然在睡眠等待I/O完成但是这个时候一个信号来的时候进程还是要被唤醒。只不过唤醒后不是继续刚才的操作而是进行信号处理。当然程序员可以根据自己的意愿来写信号处理函数例如收到某些信号就放弃等待这个I/O操作完成直接退出或者收到某些信息继续等待。
另一种睡眠是**TASK\_UNINTERRUPTIBLE****不可中断的睡眠状态**。这是一种深度睡眠状态不可被信号唤醒只能死等I/O操作完成。一旦I/O操作因为特殊原因不能完成这个时候谁也叫不醒这个进程了。你可能会说我kill它呢别忘了kill本身也是一个信号既然这个状态不可被信号唤醒kill信号也被忽略了。除非重启电脑没有其他办法。
因此这其实是一个比较危险的事情除非程序员极其有把握不然还是不要设置成TASK\_UNINTERRUPTIBLE。
于是,我们就有了一种新的进程睡眠状态,**TASK\_KILLABLE可以终止的新睡眠状态**。进程处于这种状态中它的运行原理类似TASK\_UNINTERRUPTIBLE只不过可以响应致命信号。
从定义可以看出TASK\_WAKEKILL用于在接收到致命信号时唤醒进程而TASK\_KILLABLE相当于这两位都设置了。
```
#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开头。我这里举几个例子。
```
#define PF_EXITING 0x00000004
#define PF_VCPU 0x00000010
#define PF_FORKNOEXEC 0x00000040
```
**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;
```
## 总结时刻
这一节我们讲述了进程管理复杂的数据结构我还是画一个图总结一下。这个图是进程管理task\_struct的结构图。其中红色的部分是今天讲的部分你可以对着这张图说出它们的含义。
![](https://static001.geekbang.org/resource/image/01/e8/016ae7fb63f8b3fd0ca072cb9964e3e8.jpeg)
## 课堂练习
这一节我们讲了任务的状态,你可以试着在代码里面搜索一下这些状态改变的地方是哪个函数,是什么时机,从而进一步理解任务的概念。
欢迎留言和我分享你的疑惑和见解,也欢迎你收藏本节内容,反复研读。你也可以把今天的内容分享给你的朋友,和他一起学习、进步。
![](https://static001.geekbang.org/resource/image/8c/37/8c0a95fa07a8b9a1abfd394479bdd637.jpg)