本文为宋宝华《Linux 的进程, 线程以及调度》学习笔记.
1 进程概念
1.1 进程与线程的定义
操作系统中的经典定义:
进程: 资源分配单位.
线程: 调度单位.
操作系统中用 PCB(Process Control Block, 进程控制块)来描述进程. Linux 中的 PCB 是 task_struct 结构体.
1.2 进程生命周期
1.2.1 进程状态
R, TASK_RUNNING: 就绪态或者运行态, 进程就绪可以运行, 但是不一定正在占有 CPU
S, TASK_INTERRUPTIBLE: 浅度睡眠, 等待资源, 可以响应信号, 一般是进程主动 sleep 进入的状态
D, TASK_UNINTERRUPTIBLE: 深度睡眠, 等待资源, 不响应信号, 典型场景是进程获取信号量阻塞
Z, TASK_ZOMBIE: 僵尸态, 进程已退出或者结束, 但是父进程还不知道, 没有回收时的状态
T, TASK_STOPED: 停止, 调试状态, 收到 SIGSTOP 信号进程挂起
1.2.2 进程创建与消亡相关 API
1) system()
通过调用 shell 启动一个新进程
2) exec()
以替换当前进程映像的方式启动一个新进程
3) fork()
以复制当前进程映像的方式启动一个新进程, 子进程中 fork()返回 0, 父进程 fork()返回为子进程 ID.
4) wait()
父进程挂起, 等待子进程结束.
5) 孤儿进程与僵尸进程
孤儿进程: 一个父进程退出, 而它的一个或多个子进程还在运行, 那么那些子进程将成为孤儿进程. 孤儿进程将被 init 进程 (进程号为 1) 所收养, 并由 init 进程对它们完成状态收集工作. 孤儿进程不会浪费资源.
僵尸进程: 一个进程使用 fork 创建子进程, 如果子进程退出, 而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息, 那么子进程的进程描述符仍然保存在系统中. 这种进程称之为僵尸进程. 僵尸进程浪费系统资源(进程描述符 task_struct 存在, 进程占用的资源被回收, 不存在内存泄漏, 实际上基本不浪费系统资源, 参宋宝华的课程).
避免僵尸进程:
僵尸进程产生原因:
1, 子进程结束后向父进程发出 SIGCHLD 信号, 父进程默认忽略了它;
2, 父进程没有调用 wait()或 waitpid()函数来等待子进程的结束.
避免僵尸进程方法:
1, 父进程调用 wait()或者 waitpid()等待子进程结束, 这样处理父进程一般会阻塞在 wait 处而不能处理其他事情.
2, 捕捉 SIGCHLD 信号, 并在信号处理函数里面调用 wait 函数, 这样处理可避免 1 中描述的问题.
3,fork 两次, 父进程创建儿子进程, 儿子进程再创建一个孙子进程, 然后儿子进程自杀, 孙子进程成为孤儿进程被 init 进程收养.
1.3 进程间通信
1) 信号
信号这里指的是事件. 比如按 CTRL-C 组合键会发送 SIGINT 信号, 进程里可以捕捉到这个信号进行相应处理.
2) 管道 PIPE
一切皆文件, 管道的操作也是类似文件的操作.
popen()函数类似于 fopen()函数, 返回的是对象指针.
pipe()函数类似于 open()函数, 返回的是对象描述符.
管道是在亲属进程 (同一父进程创建出的进程) 之间进行数据传输的.
3) 命名管道 FIFO
命名管道可用于在无亲属关系之前是进程间通信.
mkfifo()/mknod()将在文件系统中创建一个有路径和名称的文件. 把这个管道文件当作普通文件用就行了, 就可以实现进程间通信.
4) 信号量
信号量, 消息队列, 共享内存是 System V IPC 机制.
临界区: 任何时刻只能有一个进程进行独占式访问的代码区.
信号量: 大部分进程间通信只需要二进制信号号, 因此这里只讨论二进制信号量. 进入临界区前, 执行 P 操作(若信号量大于 1 则减 1 并进入临界区, 否则挂起本进程); 退出临界区时, 执行 V 操作(若有进程在等待挂起则唤醒之, 否则信号量加 1).
互斥量: 互斥信号量是二进制信号量的一个子集.
5) 消息队列
与命令管道类似, 但不必考虑打开 / 关闭管道的复杂操作. 消息队列独立于进程而存在.
6) 共享内存
需要通信的进程间共享一块内存进行数据交换.
2 进程线程的实现本质
Linux 调度器实际是识别 task_struct 进行调度.
无论进程线程, 底层都对应一个 task_struct, 进程和线程的区别是共享资源的多少, 两个进程间完全不共享资源, 两个线程间共享所有资源.
2.1 fork()
执行 fork 后, 父进程的 task_struck 对拷给子进程, 父子进程最初资源完全一样, 但是两份不同拷贝, 因此任何改动都造成二者的分裂.
父子进程对内存资源 (mm) 的管理使用了 COW(Copy-On-Write, 写时拷贝)技术:
1) 在 fork 之前, 一片内存区对应一份物理地址和一份虚拟地址, 内存区的权限的 RW;
2) 在 fork 之后, 父子进程看到的内存区虚拟地址相同, 物理地址也相同, 父子进程使用的其实是同一片物理内存, 未发生内存拷贝, 操作系统会将此内存区权限改为 RO;
3) 父或子进程对内存区执行写操作将触发 PageFault, 操作系统此时会将内存区拷贝一份, 父子进程看到的虚拟地址仍然一样, 但是物理地址已经不则. 各进程虚拟地址到物理地址的映射由 MMU(Memory Management Unit, 内存管理单元)管理.
fork 运行在有 MMU 的 CPU 上.
2.2 vfork()
对于无 MMU 的 CPU, 无法应用 COW, 无法支持 fork.
无 MMU 的 CPU 使用 vfork 创建进程, 父进程将一直阻塞直到子进程 exit 或 exec.
vfork 和 fork 的本质区别是, vfork 中的父子进程共用同一片内存区.
2.3 pthread_create()
Linux 线程本质上就是进程, 只是线程间共享所有资源. 如上图所示.
每个线程都有自己的 task_struct, 因为每个线程可被 CPU 调度. 多线程间又共享同一进程资源. 这两点刚好满足线程的定义.
Linux 就是这样用进程实现了线程, 所以线程又称为轻量级进程.
2.4 PID 和 TGID
POSIX 要求, 同一进程的多个线程获取进程 ID 是得到的是唯一 ID 值.
Linux 同一进程的多线程, 在内核视角实际上每个线程都有一个 PID, 但在用户空间需要 getpid 返回唯一值, Linux 使用了一个小技巧, 引入了 TGID 的概念, getpid()返回的的 TGID 值.
进程视角的 top 命令:
不带参数的 top 命令(默认情况), 显示的是进程对单核 CPU 的利用率, 例如, 一个进程内有三个线程, 主线程创建了线程 1 和线程 2, 线程 1 和线程 2 都调用一个 while(1), 则对双核 CPU 而言, 线程 1 和线程 2 各用一个核, 占用率都是 100%, 则 top 命令看到的进程 CPU 利用率是 200%, 进程 ID 是主线程的 PID(也是 TGID).
线程视角的 top 命令:
top -H 命令从线程视角显示 CPU 占用率, 上例中, 将会显示, 线程 1 占用率 100%, 线程 2 占用率 100%.
说线程的 PID, 是指用户空间的进程 ID, 值就是 TGID; 当特别指出, 线程在内核空间的 PID, 则指线程在内核中 task_struct 里特有的 PID.
3 进程调度
3.1 实时进程调度
SCHED_FIFO: 不同优先级按照优先级高的先跑到睡眠, 优先级低的再跑; 同等优先级先进先出.
SCHED_RR: 不同优先级按照优先级高的先跑到睡眠, 优先级低的再跑; 同等优先级轮转.
内核 RT 补丁:
- /proc/sys/kernel/sched_rt_period_us
- /proc/sys/kernel/sched_rt_runtime_us
在 period 的时间里 RT 最多只能跑 runtime 的时间
3.2 普通进程调度
SCHED_OTHER:
3.2.1 动态优先级(早期 2.6)
进程有 IO 消耗性和 CPU 消耗型两种衡量参数.
优先级高的意味着: 1) 得到更多的时间片, 2) 醒时能抢占优先级低的. 时间片轮转.
内核存储静态优先级, 用户可通过 nice 来修改静态优先级.
进程的动态优先级则是根据静态优先级实时计算出来的, 调度算法奖励 IO 消耗性(调高优先级增加实时性), 处罚 CPU 消耗型(调低优先级减小实时性)
3.2.2 CFS: 完全公平调度(新内核)
红黑树, 左边节点小于右边节点的值
运行到目前为止 vruntime 最小的进程
同时考虑了 CPU/IO 和 nice
总是找 vruntime 最小的线程调度.
vruntime = pruntime/weight * 1024;
vruntime 是虚拟运行时间, pruntime 是物理运行时间, weight 权重由 nice 值决定(nice 越低权重越高), 则运行时间少, nice 值低的的线程 vruntime 小, 将得到优先调度. 这是一个随运行而动态变化的过程.
工具 chrt 和 renice:
设置 SCHED_FIFO 和 50 RT 优先级
# chrt -f -a -p 50 10576
设置 nice
- # renice -n -5 -g 9394
- # nice -n 5 ./a.out
4 多核负载均衡
略
5 参考资料
[1] 宋宝华, Linux 进程, 线程和调度
[2]
[3]
[4]
[5] https://www.cnblogs.com/yuxingfirst/p/3165407.html
来源: https://www.cnblogs.com/leisure_chn/p/10393707.html