四、内核栈
在程序执行过程中,一旦调用到系统调用,就需要进入内核继续执行。那如何将用户态的执行和内核态的执行串起来呢?这就需要以下两个重要的成员变量:
struct thread_info thread_info;
void *stack;
用户态函数栈
在用户态中,程序的执行往往是一个函数调用另一个函数。函数调用都是通过栈来进行的。函数调用其实也很简单。如果你去看汇编语言的代码,其实就是指令跳转,从代码的一个地方跳到另外一个地方。这里比较棘手的问题是,参数和返回地址应该怎么传递过去呢?
我们看函数的调用过程,A 调用 B、调用 C、调用 D,然后返回 C、返回 B、返回 A,这是一个后进先出的过程。有没有觉得这个过程很熟悉?没错,咱们数据结构里学的栈,也是后进先出的,所以用栈保存这些最合适。
在进程的内存空间里面,栈是一个从高地址到低地址,往下增长的结构,也就是上面是栈底,下面是栈顶,入栈和出栈的操作都是从下面的栈顶开始的。
内核态函数栈
接下来,我们通过系统调用,从进程的内存空间到内核中了。内核中也有各种各样的函数调用来调用去的,也需要这样一个机制,这该怎么办呢?这时候,上面的成员变量 stack,也就是内核栈,Linux 给每个 task 都分配了内核栈。在 32 位系统上arch/x86/include/asm/page_32_types.h,是这样定义的:一个 PAGE_SIZE 是 4K,左移一位就是乘以 2,也就是 8K。
内核栈在 64 位系统上 arch/x86/include/asm/page_64_types.h,是这样定义的:在PAGE_SIZE 的基础上左移两位,也即 16K,并且要求起始地址必须是 8192 的整数倍。
内核栈是一个非常特殊的结构,如下图所示:
这段空间的最低位置,是一个 thread_info 结构。这个结构是对 task_struct 结构的补充。因为 task_struct 结构庞大但是通用,不同的体系结构就需要保存不同的东西,所以往往与体系结构有关的,都放在 thread_info 里面。
五、调度
task_struct 数据结构。它就像项目管理系统一样,可以帮项目经理维护项目运行过程中的各类信息,但这并不意味着项目管理工作就完事大吉了。task_struct仅仅能够解决“看到”的问题,咱们还要解决如何制定流程,进行项目调度的问题,也就是“做到”的问题。
调度策略与调度类
在 Linux 里面,进程大概可以分成两种。
- 实时进程,也就是需要尽快执行返回结果的那种。这种进程的优先级就会比较高。
- 普通进程,大部分的进程其实都是这种。可以按照正常流程完成,优先级就没实时进程这么高。
那很显然,对于这两种进程,我们的调度策略肯定是不同的。在 task_struct 中,有一个成员变量,我们叫调度策略。
unsigned int policy;
它有以下几个定义:
#define SCHED_NORMAL 0
#define SCHED_FIFO 1
#define SCHED_RR 2
#define SCHED_BATCH 3
#define SCHED_IDLE 5
#define SCHED_DEADLINE 6
配合调度策略的,还有我们刚才说的优先级,也在 task_struct 中。
int prio, static_prio, normal_prio;
unsigned int rt_priority;
优先级其实就是一个数值,对于实时进程,优先级的范围是 0~99;对于普通进程,优先级的范围是 100~139。数值越小,优先级越高。 从这里可以看出,所有的实时进程都比普通进程优先级要高。
实时调度策略
对于调度策略,其中 SCHED_FIFO、SCHED_RR、SCHED_DEADLINE 是实时进程的调度策略。
- SCHED_FIFO 就是交了相同钱的,先来先服务,但是有的加钱多,可以分配更高的优先级,也就是说,高优先级的进程可以抢占低优先级的进程,而相同优先级的进程,我们遵循先来先得。
- 另外一种策略是,交了相同钱的,轮换着来,这就是SCHED_RR 轮流调度算法,采用时间片,相同优先级的任务当用完时间片会被放到队列尾部,以保证公平性,而高优先级的任务也是可以抢占低优先级的任务。
- 还有一种新的策略是SCHED_DEADLINE,是按照任务的 deadline 进行调度的。当产生一个调度点的时候,DL 调度器总是选择其 deadline 距离当前时间点最近的那个任务,并调度它执行。
普通调度策略
对于普通进程的调度策略有,SCHED_NORMAL、SCHED_BATCH、SCHED_IDLE。
- SCHED_NORMAL 是普通的进程,就相当于咱们公司接的普通项目。
- SCHED_BATCH 是后台进程,几乎不需要和前端进行交互。这有点像公司在接项目同时,开发一些可以复用的模块,作为公司的技术积累,从而使得在之后接新项目的时候,能够减少工作量。这类项目可以默默执行,不要影响需要交互的进程,可以降低他的优先级。
- SCHED_IDLE 是特别空闲的时候才跑的进程,相当于咱们学习训练类的项目,比如咱们公司很长时间没有接到外在项目了,可以弄几个这样的项目练练手
上面无论是 policy 还是 priority,都设置了一个变量,变量仅仅表示了应该这样这样干,但事情总要有人去干,谁呢?在 task_struct 里面,还有这样的成员变量:
const struct sched_class *sched_class;
sched_class 有几种实现:
- stop_sched_class 优先级最高的任务会使用这种策略,会中断所有其他线程,且不会被其他任务打断;
- dl_sched_class 就对应上面的 deadline 调度策略;
- rt_sched_class 就对应 RR 算法或者 FIFO 算法的调度策略,具体调度策略由进程的task_struct->policy 指定;
- fair_sched_class 就是普通进程的调度策略;
- idle_sched_class 就是空闲进程的调度策略。
这里实时进程的调度策略 RR 和 FIFO 相对简单一些,而且由于咱们平时常遇到的都是普通进程,在这里,咱们就重点分析普通进程的调度问题。普通进程使用的调度策略是fair_sched_class,顾名思义,对于普通进程来讲,公平是最重要的。
一个 CPU 上有一个队列,CFS 的队列是一棵红黑树,树的每一个节点都是一个 sched_entity,每个 sched_entity 都属于一个 task_struct,task_struct 里面有指针指向这个进程属于哪个调度类。在调度的时候,依次调用调度类的函数,从 CPU 的队列中取出下一个进程。
主动调度
例如指令 sleep 等。
抢占式调度
一个进程执行时间太长了,是时候切换到另一个进程了。
进程上下文切换
上下文切换主要干两件事情,一是切换进程空间,也即虚拟内存;二是切换寄存器和 CPU上下文。
六、进程的创建
fork 是一个系统调用,根据咱们讲过的系统调用的流程,流程的最后会在 sys_call_table中找到相应的系统调用 sys_fork。
fork 的第一件大事:复制结构
fork 做的第一件大事就是 copy_process,咱们前面讲过这个思想。如果所有数据结构都从头创建一份太麻烦了,还不如使用惯用“伎俩”,Ctrl C + Ctrl V。
fork 的第二件大事:唤醒新进程
fork 包含两个重要的事件,一个是将task_struct 结构复制一份并且初始化,另一个是试图唤醒新创建的子进程。
七、线程的创建
创建一个线程调用的是pthread_create。
用户态创建线程
线程不是一个完全由内核实现的机制,它是由内核态和用户态合作完成的。
创建进程的话,调用的系统调用是 fork,在 copy_process 函数里面,会将五大结构files_struct、fs_struct、sighand_struct、signal_struct、mm_struct 都复制一遍,从此父进程和子进程各用各的数据结构。而创建线程的话,调用的是系统调用 clone,在copy_process 函数里面, 五大结构仅仅是引用计数加一,也即线程共享进程的数据结构。