linux操作系统总览(1)——进程管理,进程协调
linux操作系统大致可分为进程管理、进程协调、内存管理、文件系统、网络管理等五大部分。本文讲述进程管理和进程协调。
进程管理
进程管理的目的是将用户和内核创建的进程/程序调度到cpu上执行,这些进程有三种
- 用户程序创建的一次性运行完的进程
- systemd等维护了常驻后台的后台进程,例如nginx,sshd
- 操作系统内核进程,例如kworker处理硬中断、信号、IO操作(包括cpu时间片硬中断调度、软中断抢占),kswapd管理页缓存和内存淘汰swap空间,watchdog检查内核死锁、防止系统卡死,ksoftirqd处理软中断,kblockd处理块设备IO,系统空闲线程(idle 线程)。这些进程一般定时执行。
为什么cpu要执行idle线程?因为CPU需要执行指令才能保持正常运行。CPU按照“取指令(Fetch)—解码(Decode)—执行(Execute)”的周期运行。如果没有指令可以取,或者取到的指令无法正确解码,CPU无法正常工作。程序计数器(PC)用于指示下一条需要执行的指令地址。如果没有明确的任务,PC可能指向无效地址,导致不可预测的行为。
进程队列
进程管理器需要把已经创建的进程放入队列中, 进程排队接受多核cpu的处理。
进程调度器有很多设计值得学习的,例如
- 就绪任务采用优先级队列组织,支持任务公平调度和任务抢占
- 存在等待队列存放需要等待较长时间,需要唤醒的任务
- 停止队列支持任务暂停和断点继续执行
- 任务执行前创建结构,执行完销毁结构
就绪队列(Ready Queue)
就绪队列是一个优先级队列。
新创建的进程会放入就绪队列Ready Queue,等待队列是进程调度的对象,将这些进程调度给CPU执行。Linux使用 CFS调度器(完全公平调度器) 管理就绪队列。CFS调度器通过一个红黑树(rb_tree)来组织进程,就绪队列中的每个进程以其虚拟运行时间作为排序依据。
调度器从红黑树中选择虚拟运行时间最小的进程进行执行。当进程时间片用完,或被抢占,进程更新虚拟运行时间后重新插入到就绪队列
就绪队列中进程的状态是R (TASK_RUNNING)
等待队列(Wait Queue)
进程在等待某些事件(如IO完成、信号到达、锁释放)时进入该队列。对应进程S状态和D状态
进程S状态TASK_INTERRUPTIBLE,可中断的睡眠状态。例如等待锁,等待socket事件。该状态进程可被调度器调度。
D状态TASK_UNINTERRUPTIBLE,不可中断的睡眠状态。该状态进程无法响应信号,无法被调度,只能等待IO执行完毕(但可以响应硬中断)。例如进程等待磁盘IO期间无法中断,原因1. 进程没必要被中断,数据到不了中断也处理不了什么 2. 简化实现,防止IO读写没有进程处理导致问题 3. 避免由于磁盘等故障导致大量进程持续中断。
对于持有spinlock的进程需要屏蔽中断,但这时候的进程不是等待状态。
停止队列(Stopped Queue)
进程被暂停(例如接收到SIGSTOP信号)后进入该队列,例如ptrace机制调试进程时会暂停某些进程,放入停止队列中。
停止队列中的进程不会被调度器调度,直到接收到恢复信号(如SIGCONT)。
僵尸队列(Zombie Queue)
包含所有处于TASK_ZOMBIE状态的进程。僵尸进程是已结束执行但尚未被其父进程回收的进程。
父进程调用wait族函数等待子进程执行完并回收时,僵尸进程会被从队列中移除。
TASK_DEAD状态的进程会被系统回收所有资源,释放进程控制块(PCB)。
进程优先级
实时进程优先级:范围从 1 到 99(高优先级)。实时进程调度时,高优先级的进程必须执行完毕或主动放弃 CPU,低优先级的进程才能运行。
普通进程优先级:范围从 100 到 139(低优先级)。默认优先级为 120。普通进程使用 完全公平调度器(CFS),CFS中,执行时间少的进程在计算时会有较高的动态优先级。
普通进程优先级
可以通过 nice 或 renice 命令设置普通进程的静态优先级。范围:-20(最高优先级)到 19(最低优先级)。nice默认值为 0。
nice 值越低,进程优先级越高。
内核线程是由内核创建并运行的任务,通常用于关键系统功能,优先级由内核自行分配。
1 | 进程类别 优先级范围 优先级特点 |
系统服务和守护进程通常负责后台任务,默认优先级设置中等,以确保它们不会过多占用 CPU,但仍能及时响应。
1 | 进程类别 默认 Nice 值 优先级特点 |
完全公平调度CFS
- 操作系统统计周期内每个进程的执行时间,这个执行时间是个虚拟执行时间vriture_runtime,计算时会考虑进程优先级,优先级高的进程统计的执行时间会偏少。
- 执行时间少的进程在下个周期优先执行
进程切换,Linux内核使用定时器硬中断来实现时间片轮转调度。当定时器中断发生时,内核会检查当前运行的进程是否超过其时间片。是则进行进程切换。定时器中断的频率通常为100Hz或1000Hz,表示每隔10ms或1ms中断一次。
进程协调
进程协调的手段包括两个部分
- 协调进程切换,包括信号、硬/软中断、系统调用
- 协调进程/线程同步,包括锁、条件变量、信号量等
协调进程切换
系统调用
当进程从执行用户代码转向执行内核代码时,就发生系统调用。需要注意该过程不发生进程切换。
- 进入内核代码前,进程首先调用
int $0x80
或syscall指令
,该指令会让CPU转向执行(entry_INT80_32或entry_SYSCALL_64)汇编代码,即根据系统调⽤号调⽤对应的内核处理函数。 - 执行内核处理函数前,cpu 将当前寄存器内容保存到用户栈,使用内核栈执行内核处理函数
- 内核处理函数执行完后,调用sysret 指令从用户栈恢复寄存器内容,使用用户栈继续执行用户程序
为什么执行内核代码需要调用int $0x80
或syscall指令,而不是直接函数调用的形式执行?
因为系统调用状态的保存是CPU硬件实现的,CPU的两个寄存器
MSR_LSTAR:保存内核系统调用入口的地址。
MSR_STAR:保存用户态和内核态的代码段选择器。
在syscall指令中,寄存器保存用户栈,执行内核函数,恢复用户栈都是硬件实现的。只需要调用CPU指令。操作系统只需要配置相关寄存器然后调用指令即可。
在int $0x80
中,寄存器保存用户栈,执行内核函数,恢复用户栈是操作系统软件实现的(性能差于syscall,现代CPU基本使用后者),操作系统把处理函数注册到中断向量表IDT的0x80位置。当进程执行int $0x80指令,CPU会自动从寄存器找到IDT地址,转向执行处理函数(IDT的地址存放在寄存器IDT 寄存器(IDTR))。“CPU从寄存器找到IDT,转向执行处理函数”这是硬件实现的。
硬中断和中断向量表
上面说到,系统调用可以通过发起int $0x80
指令,CPU自动从中断向量表找到处理函数。中断向量表IDT是中断执行的核心,中断函数由操作系统注册,但无法直接执行,只能借助cpu中断表执行。中断优先级与向量号直接相关,向量号越低优先级越高。
硬中断和中断向量表,中断的对象是CPU。通过CPU引脚信号/CPU指令来触发中断。硬中断可以抢占CPU,一般来说,如果用户态进程发生了硬中断/异常,执行硬中断处理函数的进程和用户态进程是一个。如果硬中断不是由用户态进程产生,例如时间片用尽、磁盘网络IO中断,中断处理函数由kworker 执行。
1 | /* |
0-31中保存的是异常的中断向量,这些异常包括除0、断点调试、边界溢出、段错误、浮点数异常等,中断处理会转向执行查找进程对应的异常处理函数,默认是异常退出进程(向进程发SIGINT信号,程序异常退出)
为什么使用中断处理异常?因为异常是运行态的,程序编译后并不知道异常会不会执行。当CPU执行遇到异常后,不能继续执行原程序的指令。 只能去IDT执行异常函数的处理指令。
CPU转向执行异常,需要1. 停止当前指令执行; 2. 保存 CPU 状态; 3. 跳转到指定的处理程序处理。
CPU/进程上下文切换也是通过硬中断实现,同样需要1. 停止当前进程指令执行; 2. 保存 CPU 状态; 3. 跳转转型下一个进程的指令。CPU/进程上下文是否切换 等价于 是否出现硬中断。
1 | /* Interrupts/Exceptions */ |
向量 32 到 127,分配给硬件中断和其他系统用途。硬件中断的触发,连接cpu引脚的硬件会向引脚发送信号,cpu收到信号会立即暂停当前程序处理(屏蔽中断的除外),从idt中找到硬件中断处理函数执行。
对需要较长时间处理的中断,例如网卡磁盘收发数据。为了时效性,硬件中断处理函数只是创建一个异步任务到队列就返回,任务具体的处理由后续软中断负责。
1 | IRQ 中断号 用途 |
向量 128(0x80),用于系统调用(如 int $0x80)。
向量 129 到 255,用户自定义的硬中断。例如,int 0x81:某些调试工具可能使用该中断来与内核交互;int 0xFF:某些实时系统可能定义为特殊的系统功能调用;APIC 定时器通常分配到高向量。
软中断
软中断和软中断向量表,是内核软件层实现的(软中断号,处理函数)的映射,目的是异步任务延迟调度,没有直接影响到CPU,不抢占CPU。软中断和是否出现CPU/进程上下文切换没有联系。
软中断通过内核代码调用 raise_softirq() 触发。主要可以分为1. 定时器(很重要,比如处理超时锁等待,sleep超时) 2. 网络发送接收 3. 块设备IO 4. Tasklet 等类型。Tasklet是用户注册的一种函数,该函数和软中断一起被执行。
每个cpu维护一个软中断队列,软中断的执行不会切换CPU。如果CPU软中断占用时间长,可能是网络包是大量的小包,也可能是磁盘/网络处理慢。
1 | 软中断号 名称 用途 |
软中断的处理时机
- 硬中断处理完成后,内核会检查是否有挂起的软中断。如果有则执行
- 在某些内核路径中,内核会主动调用 do_softirq() 来检查和执行挂起的软中断。例如网络协议栈处理数据包时,通过 net_rx_action() 调用软中断;高精度定时器中,触发 hrtimer 相关软中断。
软中断的优先级高于普通任务。当软中断执行时间较长、内核会将剩余未处理的软中断交给专用线程 ksoftirqd 来执行。
信号
软中断和信号的区别是,软中断是CPU级别的,软中断得到的网络包、磁盘数据包需要另外拷贝到进程空间才能被进程使用。而信号是针对进程的。也就是说,操作系统为每个CPU维护一个软中断任务队列,但每个进程都维护了自己的信号任务队列。
信号执行的时机
- 进程从系统态返回到用户态的前夕,也就是一个用户态的进程由于系统调用、硬中断(进程切换)或异常而进入系统空间,执行完后返回用户态前,处理信号
- 进程在进入等待队列后刚被唤醒的时候
信号可以由内核发起(内核想影响某进程的执行,主要是通知某进程终止)就给该进程发个信号,虽然都是通知进程退出,但退出原因不同。退出原因可以是终端中断,中断退出,杀死,运算错误,段错误等。
1 | #define SIGHUP 1 终端挂起或控制进程终止 |
磁盘/网络数据可读可写也可以通过SIGIO信号通知到进程,也就是所说的“信号驱动式IO”。但更广泛的是通过事件通知。除了信号驱动和事件通知(非阻塞IO),进程默认是阻塞等待数据可读可写,这是通过软中断实现。软中断获得数据并把数据拷贝到进程空间后,会唤醒正在睡眠的进程,将进程从等待队列转到就绪队列。
事件通知
采用阻塞IO的进程会有先进入等待队列,数据就绪后重新进入就绪队列的逻辑。如果采用非阻塞IO,进程不会进入等待队列。
linux的字符设备/块设备/网络IO ,在进程中可以用文件描述符fd标识。设置文件描述符为非阻塞模式(O_NONBLOCK)从而采用非阻塞IO。
文件描述符可以与多个事件关联,这些事件表示文件描述符的状态变化,例如数据可读、可写或发生错误。linux 内部采用了事件通知的模型
- 消费者(进程)注册感兴趣的事件和事件回调函数,例如进程注册感兴趣的读写事件
- 软中断将数据拷贝到用户进程空间后,会匹配注册的事件,执行对应的事件处理函数。
- 如果是select/poll,事件处理函数就是把事件设置到select 列表对应的fd,等待进程轮询找到满足事件的fd
- 如果是epoll,事件处理函数就是从红黑树找到对应等待的文件描述符,将fd转移到就绪队列,唤醒阻塞在epoll_wait等待的进程。epoll_wait还会通过超时解除阻塞。
事件通知驱动和主动轮询驱动 对协调模块的设计,例如消息队列, 也具有很大参考意义
锁
锁是协调多个任务对临界区的执行,锁保证,一个任务要么独占执行完临界区,要么不执行临界区,不允许临界区同时被多个任务执行。
单机锁主要有三种,自旋锁,互斥锁,读写锁。自旋锁是互斥锁一种特殊实现
- spinlock 主要是保护多cpu操作对象的安全,可以是cpu共享的硬件变量,为了防止死锁,加spinlock需要屏蔽当前cpu的中断。为了防止死锁,spinlock需要关中断和禁止抢占
- mutex,存放在内存的共享变量,维护多线程的安全
- 读写锁,使用count实现。读锁count-1,写锁count-很大的magic number。通过count值能知道目前持有读锁还是写锁。读写锁问题是读操作不需要等待,如果对象持续被读,写会被饿死。
本文标题:linux操作系统总览(1)——进程管理,进程协调
文章作者:Infinity
发布时间:2024-11-06
最后更新:2025-01-11
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 3.0 CN 许可协议。转载请注明出处!