上回书说到Linux进程的由来和Linux进程的创建,其实在同一时刻只能支持有限个进程或线程同时运行(这取决于CPU核数量,基本上一个进程对应一个CPU),在一个运行的操作系统上可能运行着很多进程,如果运行的进程占据CPU的时间很长,就有可能导致其他进程饿死。为了解决这种问题,操作系统引入了进程调度器来进行进程的切换,轮流让各个进程使用CPU资源。
1. Linux调度架构的核心概念:
1)rq:进程的运行队列(runqueue),每个CPU对应一个,包含自旋锁(spinlock)、进程数量、用于公平调度的CFS信息结构、当前运行的进程描述符等。实际的进程队列用红黑树来维护(通过CFS信息结构来访问)。
2)cfs_rq:cfs调度的进程运行队列信息,包含红黑树的根结点、正在运行的进程指针、用于负载均衡的叶子队列等。
3)sched_entity:把需要调度的东西抽象成调度实体,调度实体可以是进程、进程组、用户等。这里包含负载权重值、对应红黑树结点、虚拟运行时vruntime等。
4)sched_class:把调度策略(算法)抽象成调度类,包含一组通用的调度操作接口。接口和实现是分离,可以根据调度接口去实现不同的调度算法,使一个Linux调度程序可以有多个不同的调度策略。
2. Linux进程通过schedule函数来切换的,主要分为以下几个阶段:
1)关闭内核抢占,初始化部分变量。获取当前CPU的ID号,并赋值给局部变量CPU,使rq指向CPU对应的运行队列。标识当前CPU发生任务切换,通知RCU更新状态,如果当前CPU处于rcu_read_lock状态,当前进程将会放入rnp-> blkd_tasks阻塞队列,并呈现在rnp-> gp_tasks链表中。关闭本地中断,获取所要保护的运行队列的自旋锁,为查找可运行进程做准备。
2)检查prev的状态,更新运行队列。如果不是可运行状态,而且在内核态没被抢占,应该从运行队列中删除prev进程。如果是非阻塞挂起信号,而且状态为TASK_INTER-RUPTIBLE,就把该进程的状态设置为TASK_RUNNING,并将它插入到运行队列。
3)task_on_rq_queued(prev) 将pre进程插入到运行队列的队尾。
4)pick_next_task选取将要执行的next进程。
5)context_switch(rq, prev, next)进行进程上下文切换。
3. 进程切换的场景有以下3种:
1) 该进程分配的CPU时间片用完。
2) 该进程主动放弃CPU(例如IO操作)。
3) 某一进程抢占CPU获得执行机会。
Linux并没有使用x86 CPU自带的任务切换机制,需要通过手工的方式实现了切换。
4. 调度中的CPU亲和度
进程创建后在内核的数据结构为task_struct , 该结构中有掩码属性cpus_allowed,4个核的CPU可以有4位掩码,如果CPU开启超线程,有一个8位掩码,进程可以运行在掩码位设置为1的CPU上。
Linux内核API提供了两个系统调用,让用户可以修改和查看当前的掩码:
1) sched_setaffinity():用来修改位掩码。
2) sched_getaffinity():用来查看当前的位掩码。
在下次task被唤醒时,select_task_rq_fair根据cpu_allowed里的掩码来确定将其置于哪个CPU的运行队列,一个进程在某一时刻只能存在于一个CPU的运行队列里。
在Nginx中,使用了CPU亲和度来完成某些场景的工作:
worker_processes 4;
worker_cpu_affinity 0001001001001000;
上面这个配置说明了4个工作进程中的每一个和一个CPU核挂钩。如果这个内容写入Nginx的配置文件中,然后Nginx启动或者重新加载配置的时候,若worker_process是4,就会启用4个worker,然后把worker_cpu_affinity后面的4个值当作4个cpu affinity mask,分别调用ngx_setaffinity,然后就把4个worker进程分别绑定到CPU0~3上。
worker_processes 2;
worker_cpu_affinity 01011010;
上面这个配置则说明了两个工作进程中的每一个和2个核挂钩。
参考资料:陈科的《Linux内核分析及应用》1.3 Linux进程的调度