Linux 源码是 C 写的,这里为了描述方便,C 语言里的 struct 就记成对象。
进程和线程
进程在源码里面是一个 task_struct 对象,它的相关描述是 thread_info 对象。方便找到 进程 对象 的位置并修改。有的服务器还会再CPU的寄存器存放进程的指针,用于加快访问速度。
进程有5种状态,保存在 task_struct 的 state 对象里面。
- TASK_RUNNING
- TASK_INTERRUPTIBLE (可中断)
- TASK_UNINTERRUPTIBLE (不可中断)
- _TASK_TRACED (被其他进程追踪的进程)
- _TASK_STOPPED (停止)
Unix系统的进程存在一个明显的继承关系,所有的进程都是 PID 为 1 的 init 进程的后代。系统中每个进程必有一个父进程,也可以拥有零个或多个子进程。
Linux 的 fork 实现采用 写时拷贝(copy-on-write) 技术,也就是懒加载。开始fork进程的时候并不会把数据也fork给子进程,开始是父子进程共享地址空间数据,只有当子进程开始写数据的时候,才开始把父子进程的地址空间分离开来。
Linux对线程的处理就是当作普通的进程(轻量级进程)来处理,只不过它与其他的一些进程共享某些资源(地址空间)。在创建的时候,需要指明它与父进程所共享的资源(共享地址空间,文件系统资源,文件描述符,信号处理程序)。
进程调度
Linux 提供的是抢占式的多任务模式,这种模式调度程序就会根据进程的优先级来判断进程什么时候该停止,什么时候该运行。这个强制停止的操作就称为抢占。每个进程可执行的时间都是设置好的,这个叫做进程的时间片。所以,有效的设置时间片对调度是非常有帮助的。linux 的公平调度本身并没有采取时间片来实现。
Linux 的公平调度算法名字叫 CFS,是O(1) 调度的升级版。Linux 的进程有两种优先级范围,实时级优先级是 0-99, 数字越大优先级越高。普通进程是 nice 值表示-20 ~ 19,数字越低权限越大。一个进程不可能同时属于实时级优先级和 nice 值优先级。使用命令可以查看(rtprio表示实时级优先级, ni表示nice值. )
ps -eo state,uid,pid,ppid,rtprio,ni,time,comm
Linux 的公平调度表现在会给相同优先级的进程分配相同的 “时间片”,在 Linux 中表示为处理器使用比。两个相同优先级的进程,初始时都是 50 %, 假设一个是 I/O 密集型任务,一个是 CPU 密集型任务,当I/O阻塞时, CPU这个任务一直在运行,当 I/O 不阻塞时, CPU任务就会立刻阻塞,让I/O任务先处理,这是因为 I/O 阻塞地 时候 处理器使用少,所以优先调度。这才体现了公平调度。读到这里,我不禁感叹一句,真的好牛逼。
Linux 的进程调度是通过一系列进程调度类来实现的,CFS 对应的是普通进程(nice值)的调度类,CFS 的调度不是基于根据nice值等比例的时间片来实现的,首先,会根据进程总数和每个进程的优先级得出每个进程的CPU运行比,有最小时间粒度,是 1ms。当进程过多时,就会出现频繁的进程切换带来的性能损耗,但是实际运行情况还好,没有到那么多进程的程度。
Linux 进程调度实现:
- 时间记账。CFS 通过 vruntime 变量来追踪进程的运行时间
- 进程选择。所有可运行进程保存在一个自平衡的二叉搜索树上面(红黑树)上面,每次取进程就找数的最左节点,为了保证性能,会缓存最左节点的进程信息,方便快速找到。
- 调度器入口。 schedule() 函数实现,从优先级高到低选择调度类的实现,每个调度类都会维护一个可运行队列。具体看源码实现吧
- 睡眠和唤醒。当进程需要阻塞或者不可用的时候,会从树中把这个进程移除,修改进程的状态,加入到一个等待队列中去。当有事件触发时,修改进程状态,并加入到可运行队列中去(红黑树)了。
Linux 内核是支持 SMP 的,这里来谈谈它的抢占式特点。抢占分为两种,一种是用户抢占,发生在进程执行完系统调用返回到用户空间 或 中断程序返回用户空间 的时候,会被检查是否设置了 need_resched 标志,当这个标志被设立了,就会调用一次 schedule() 方法执行下一个进程。另一种是内核抢占,内核抢占还会加上另一个锁标志判断,preempt_count, 默认是0,加锁就 +1,释放锁就 -1。只有当 need_sched 被设置了且 preempt_count 等于 0 才会进行进行 schedule() 调用。内核抢占发生在以下几种情况
- 中断程序正在运行,且返回用户空间
- 内核代码主动调用 schedule()
- 内核代码再一次具备可抢占条件的时候
- 内核代码中的任务阻塞也会这样
Linux的调度策略,对于实时进程,有 SCHED_FIFO 和 SCHED_RR 两种,后者比前者优先级高,在每一种实时进程里面,只能高优先级的抢占低优先级的。SCHED_FIFO 永远也不能抢占 SCHED_RR 类型的进程。实时进程是有时间片的,只能等待时间片执行完毕或者进程主动阻塞才能切换别的进程执行。实时进程的进程值从0-99,普通进程是 SCHED_NORMAL 调度策略,进程值100 ~ 139 ,对应于nice 值的 -20 ~ 19。
系统调用
Linux 在硬件设备和用户空间之间加了一层 系统调用 使得用户程序访问硬件设备更安全,可靠。
Linux 内核的系统调用实现有三步:
- 确定系统调用号
- 系统调用方式
- 参数传递方式
内核为每个系统调用都有一个唯一的系统调用号,存在于 sys_call_table 中。按顺序排序,每种硬件设备都有对应的table。用户程序会通过 软中断 来告知内核需要进行一个系统调用, 这个软中断就是个 0x80 的指令异常,内核会专门处理这个异常然后进行相应的系统调用。这个处理程序就是 system_call(), 现在已经换成了效率更高的 sysenter 指令了。在 x86 上面,系统调用号是通过在 eax 寄存器传递个内核的,所以参数也是一样,不过他们放在寄存器的 ebx, ecx, edx, esi, edi 等32位的寄存器上(in order)。
实现系统调用,这一段没啥说的,只能说一句,我们写代码需要像内核一样的稳定。
锁
一个支持 SMP 机器的系统肯定是得有锁的。锁有多种,大致分两类,一类是阻塞式锁,一类是非阻塞式锁。
非阻塞形锁:
原子性操作: 这其实不算是锁,只不过是cpu支持的一种指令,是原子性的。有些是CPU原子级别支持,有些只是通过锁总线来支持的。
自旋锁:未获取到锁的进程就一直在等待队列里面自旋,空转CPU。适用于锁内执行时间较短的任务。否则空转cpu太浪费资源。
阻塞形锁:
- 信号量: 可计数的,实现原理是让让等待中的线程睡眠,等待信号唤醒
- mutex(互斥锁):默认计数为1的信号量
- 顺序锁
- BLK(大内核锁)
每种锁都有 读写锁的适配版本,读的时候可以并发读,写的时候只能一个锁持有。
应用场景:
在中断上下文里面只能用自旋锁,因为中断上下文不能让线程睡眠。只能在进程上下文里面使用阻塞形锁。
这里有一篇关于中断上下文和进程上下文的概念文章 https://www.cnblogs.com/stemon/p/5148869.html
网友评论