从操作系统角度来看,进程是资源分配的基本单位,线程是调度的基本单位。进程有独立的地址空间,线程共享进程的内存地址空间。内核通过进程控制块PCB来维护进程相关信息。Linux内核的PCB是task_struct结构体。
struct task_struct {
pid_t pid;
struct mm_struct *mm;
/* Filesystem information: */
struct fs_struct *fs;
/* Open file information: */
struct files_struct *files;
/* Signal handlers: */
struct signal_struct *signal;
...
};
$cat /proc/sys/kernel/pid_max
131072
:(){:|:&};:
fork炸弹通过管道创建子进程,进程数量指数级增长,进程表饱和后就无法创建新的进程,直到进程表中的某一进程终止,但是fork炸弹进程会竞争创建新的fork炸弹进程,最后基本不能运行新的进程。forK炸弹会占用大量的CPU和内存,导致系统响应速度骤降。
就绪
进程已经分配到除CPU之外的所有必要资源,只要获得CPU资源便可以执行。
运行
进程已经获得CPU资源,进程正在处理机上执行。
浅度睡眠
进程处于睡眠状态,正在等待资源的分配,可以被信号唤醒。
深度睡眠
进程处于睡眠状态,正在等待资源的分配,但不可以被信号唤醒。
进程在执行过程中,没有命中代码段,此时会发生page fault,进程进入深度睡眠。
停止
进程已经停止执行,接收到SIGSTOP和SIGTSTP等信号时,进程进入停止状态,直到收到SIGCONT信号后,进程再次变得可运行。
cpulimit 原理在于通过周期发送SIGSTOP和SIGCONT来控制CPU使用率
僵尸
进程已经终止,资源已经释放,但task_struct还在,等待父进程收集的统计信息。
pid_t fork(void);
fork创建子进程,父进程返回子进程的PID,子进程返回值为0,fork错误则返回负值。系统为子进程新建task_struct,子进程的task_struct除了pid,ppid之外,继承了父进程task_struct的大部分内容。子进程基本上是父进程的一个副本,包括真实有效的UID,GID,进程组和会话ID,环境,资源打开的文件,共享的进程段。但注意的是,子进程没有继承父进程的超时设置,父进程的文件锁,未决信号。当父进程或子进程对资源进行任何修改,资源就会发生分裂,发生写时拷贝。例如chroot, memory, mmap。
pid_t vfork(void);
vfork也是用来创建子进程,vfork和fork的不同之处在于vfork的父进程会被阻塞,子进程exit或调用exec之后,父进程才被调度运行。vfork子进程和父进程共享数据段,子进程的对虚拟地址空间任何修改都对父进程可见。从进程控制块来看,子进程和父进程共享task_struct中的 mm_struct。vfork最初是因为fork没有实现COW机制而出现,fork的COW需要内存管理单元MMU的支持。
int clone(int (*fn)(void *), void *child_stack,
int flags, void *arg, ...
/* pid_t *ptid, struct user_desc *tls, pid_t *ctid */ );
clone是linux为了创建线程设计的,可以有选择性的继承父进程的资源。clone创建出来的和父进程共享所有资源,那么创建出来的就是线程。clone也可以将创建出来的进程变成父进程的兄弟进程。
fork前,数据段的页表权限是读写(R+W)。fork之后,系统将数据段的页表权限设置为只读权限(RD-ONLY),所以父进程和子进程看到的虚拟地址和物理地址是一样的。无论子进程还是父进程对RD-ONLY的页表进行写操作,CPU都会收到一个缺页中断(page fault). 这个时候CPU去申请一新的物理页框(page),把原来的页框数据拷贝到新的物理页框,并且把原来的数据段的页表权限设置为读写(R+W)。这样就完成了COW,父进程和子进程的相同虚拟地址所多对应的物理地址并不一样。
fork后COW前后变化的情况如下:
- | 虚拟地址 | 物理地址 | 权限 |
---|---|---|---|
fork前 | virt1 | phy1 | R+W |
fork后 | virt1 | phy1 | RD-ONLY |
write后 | virt1 | phy2 | R+W |
调度器设计有两个目标,一个是高吞吐率和快响应。追求高吞吐率,必然要求做更多的有用功,而不是频繁的在频繁切换上下文上浪费时间。要求快响应,势必要求更高优先级进程能随时抢到CPU,但抢占会引起上下文切换,上下文切换会引起大量的cache miss。因此,为了响应快就需要抢占,抢占了就会导致吞吐率下降。调度器需要在两者之间做一个平衡。
#define MAX_USER_RT_PRIO 100
#define MAX_RT_PRIO MAX_USER_RT_PRIO
#define MAX_PRIO (MAX_RT_PRIO + 40)
typedef struct prio_array prio_array_t;
struct prio_array {
unsigned int nr_active; //本进程中进程的个数
unsigned long bitmap[BITMAP_SIZE]; //进程队列queue[MAX_PRIO]的索引位图
struct list_head queue[MAX_PRIO]; //各个优先级所对应的队列
};
O(1)算法的核心数据结构是优先级数组,MAX_PRIO的最大取值为140,因此进程的优先级最大值为139,但优先级的值越小,表示优先级越高。实时进程使用0到99的优先级,普通进程的优先级为100到139。queue数组中包含140个可运行的进程链表,相同优先级的进程被放到queue[prio]的链表中。当某个优先级的进程处于可运行状态时,bitmap中该优先级所对应的位就被置1。因此通过查找bitmap中第一个被置1的位就能达到O(1)找出最高优先级进程。
实时进程的优先级不会动态修改,但比普通进程的优先级高。实时进程采用SCHED_FIFO和SCHED_RR调度策略,两种调度策略都是高优先级先运行,区别是优先级相同时SCHED_FIFO采用先进先出运行,SCHED_RR采用时间片轮转运行。
普通进程采用SCHED_NORMAL调度策略,不同类型进程有不同的优先级,每个进程有一个静态优先级。优先级高的进程获得更长的时间片,而优先级低的进程得到的时间片则较短。进程除了拥有静态优先级外,还有动态优先级。动态优先级以静态优先级为基础,再加上相应的惩罚或奖励。惩罚或奖励根据进程过去平均的睡眠时间来决定。
从Linux2.6.23内核开始采用CFS(completely fair schedule)调度算法。
CFS最求的是CPU资源的公平分配,为了实现公平调度,内核给每个进程维护了一个虚拟运行时间vruntime。
Virtual runtime = (实际运行时间) * 1024/weight
其中nice值越小,weight越大,因此优先级高的进程vruntime增加得慢。调度实体被系统用红黑树组织起来,key为进程的vruntime。红黑树左边节点的vruntime小于右边节点的vruntime,如果进程得到执行,就会开启对vruntime的跟踪,在tick中断到来前更新vruntime,从而调度器可以不断的检查目前系统的公平性。CFS选择vruntime最小的进程来调度(如下图),因此总是选择最左边的节点做为下一个获得CPU的任务,读取操作的时间复杂度为O(LgN)。
调度延迟
对于两个vruntime差不多的进程,可能每个tick都会导致这两个进程的相互切换。例如A进程运行一个tick后,vruntime大于B,于是切换到B进程。B进程运行一个tick后,vruntime又超过了A进程,于是又切换回A进程,如此反复。 为了权衡公平与性能,CFS引入了调度延迟的概念。
CFS模块化
CFS提供了调度器模块管理器,调度器算法可以作为模块注册到该管理器中。
不同的进程可以选择不同的调度模块。如实时进程使用实时调度模块,普通进程则使用CFS调度模块。
组调度是为了控制调度的粒度,比如希望每个用户占用相同的cpu时间,但是每个用户的进程数量不同,以进程为粒度的调度,会导致进程数多的用户占有的CPU多。因此引入了组调度。
chrt [options] priority command [argument...]
chrt [options] -p [priority] pid
renice [-n] priority [-g|-p|-u] identifier...
在SMP环境下,每个CPU有一个run_queue队列。这样做的好处是尽量使进程在同一个CPU上运行,每个CPU上的调度程序只访问自己的run_queue,避免了竞争。但这样也造成了一些CPU闲着,一些CPU忙不过来的情况,负载均衡需要做的是,在一定的时机下,将进程从一个run_queue迁移到另一个run_queue上,来保持CPU之间的负载均衡。
实时进程
实时进程严格按照优先级来进行,N个CPU上运行必须是优先级最高的TOP-N个进程。如果TOP-N个进程中,有一个离开TASK_RUNNING状态或者因为优先级被调低而退出TOP-N,那么N+1优先级的进程度就会被调入TOP-N。内核遍历所有的run_queue,把这个N+1优先级进程找出来。如果一个TOP-N之外的实时进程的优先级被调高,内核需要遍历所有run_queue,将优先级处于TOP-N的进程找出来,将正在占用CPU让给新进的TOP-N进程。新进TOP-N进程和退出TOP-N进程可能并不处于同一个CPU上,在运行之前,内核需要将其迁移到退出TOP-N进程上。内核通过pull_rt_task和push_rt_task两个函数来实现实时进程的迁移.
普通进程
普通进程并不严格要求优先级关系,可以容忍一定程度的不均衡,所以普通进程的负载均衡可以采用一些一部调整策略。 当前进程离开TASK_RUNNING状态,多对应的run_queue已经没有进程可用时,触发负载均衡,试图从别的run_queue中pull一个进程来运行。每隔一定的时间,启动负载均衡过程,试图解决系统中不均衡。 对于exec的进程,地址空间已经完全重建,当前CPU上已经不再缓存对它有用的信息,内核也会考虑负载均衡,为他们寻找合适的CPU。
CPU软亲和性指的是进程不会在CPU之间频繁迁移。硬亲和性指进程只在指定的CPU上运行。linux的硬亲和性保存在进程task_struct的cpus_allowed这个位掩码标志中,每一位掩码对应一个进程可用的处理器。
int pthread_attr_setaffinity_np(pthread_attr_t *, size_t, const cpu_set_t *);
int pthread_attr_getaffinity_np(pthread_attr_t *, size_t, cpu_set_t *);
int sched_setaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);
int sched_getaffinity(pid_t pid, unsigned int cpusetsize, cpu_set_t *mask);
taskset [options] mask command [argument...]
taskset [options] -p [mask] pid
cgroups(Control Groups)是Linux内核提供的一种可以限制、记录、隔离进程组(process groups)所使用的物理资源(包括CPU,内存,IO)的机制。cgroups的API以VFS方式实现,用户可以通过文件操作实现cgroups的组织管理。
cgroups可以用cpu.cfs_period_us和cpu.cfs_quota_us来限制该组中的所有进程在单位时间里可以使用的CPU时间。这里的CFS是完全公平调度器的缩写.。
设置进程的cpu使用率为160%
root@localhost:/sys/fs/cgroup/cpu/cgroup1# pgrep xmrig
14761
root@localhost:/sys/fs/cgroup/cpu/cgroup1# ll
total 0
drwxr-xr-x 2 root root 0 May 27 22:26 ./
dr-xr-xr-x 6 root root 0 May 27 22:26 ../
-rw-r--r-- 1 root root 0 May 27 22:26 cgroup.clone_children
-rw-r--r-- 1 root root 0 May 30 17:40 cgroup.procs
-r--r--r-- 1 root root 0 May 27 22:26 cpuacct.stat
-rw-r--r-- 1 root root 0 May 27 22:26 cpuacct.usage
-r--r--r-- 1 root root 0 May 27 22:26 cpuacct.usage_percpu
-rw-r--r-- 1 root root 0 May 27 22:26 cpu.cfs_period_us
-rw-r--r-- 1 root root 0 May 27 22:28 cpu.cfs_quota_us
-rw-r--r-- 1 root root 0 May 27 22:26 cpu.shares
-r--r--r-- 1 root root 0 May 27 22:26 cpu.stat
-rw-r--r-- 1 root root 0 May 27 22:26 notify_on_release
-rw-r--r-- 1 root root 0 May 27 22:26 tasks
root@localhost:/sys/fs/cgroup/cpu/cgroup1# cat cgroup.procs
14761
root@localhost:/sys/fs/cgroup/cpu/cgroup1# cat cpu.cfs_period_us
100000
root@localhost:/sys/fs/cgroup/cpu/cgroup1# cat cpu.cfs_quota_us
160000