Linux内核之进程

进程管理

1、进程

  • 进程是处于执行期的程序以及相关资源的总称
  • 线程是在进程中活动的对象,每个线程都拥有一个独立的程序计数器,进程栈和一组进程寄存器,内核调度的对象是线程,而不是进程
  • 进程提供两种虚拟机制:虚拟处理器和虚拟内存,同一进程的线程之间可以共享虚拟内存,但每个都拥有各自的虚拟处理器
  • 父进程可以通过waii4()系统调用查询子进程是否终结,这其实使得进程拥有等待特定进程执行完毕的能力

2、进程描述符及任务结构

  • 进程描述符(包含一个具体进程的所有信息)存放在任务队列(双向循环链表)中
  • Linux通过slab分配器分配进程描述符结构struct task_struct,每个任务的struct thread_info结构在它的内核栈尾端分配,并将其task域指向该进程描述符实际的指针
  • 内核通过一个唯一的进程标识值或PID来标识每个进程,PID存放在进程描述符中,PID默认最大值32768,可以通过修改/proc/sys/kernel/pid_max来提高上限
  • 进程描述符中的state域描述了进程的当前状态,系统中的每个进程都必然处于如下五种进程状态中的一种:
    • 1) TASK_RUNNING(运行) 进程是可执行的
    • 2) TASK_INTERRUPTIBLE(可中断) 进程正在睡眠,等待某些条件的达成
    • 3) TASK_UNINTERRUPTIBLE(不可中断) 除了就算是接收到信号也不会被唤醒或准备投入运行外,这个状态与可打断状态相同
    • 4) __TASK_TRACED(被跟踪)被其他进程跟踪的进程
    • 5) __TASK_STOPPED(停止)进程停止执行
  • 一般程序在用户空间执行,当一个程序执行了系统调用或者触发了某个异常,它就陷入了内核空间,此时,我们称内核代表进程执行并处于进程上下文中。
  • 系统调用和异常处理程序是对内核明确定义的接口,进程只有通过这些接口才能陷入内核执行
  • 所有进程都是PID为1的init进程的后代,内核在系统启动的最后阶段启动init进程
  • 进程间的关系存放在进程描述符结构中,parent域指向父进程描述符,还包含一个称为children的子进程链表,可以通过这种继承体系从系统的任何一个进程出发查找到任意指定的其它进程

3、进程创建

  • fork()通过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID和某些资源、统计量
  • exec()负责读取可执行文件并将其载入地址空间开始运行
  • Linux的fork()使用写时拷贝(copy-on-write)页实现,内核此时并不复制整个进程地址空间,而是让父进程和子进程共享一个拷贝,只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝,这种优化可以避免拷贝大量根本不会被使用的数据

4、线程在Linux中的实现

  • Linux把所有的线程都当做进程来实现的,线程仅仅被视为一个与其它进程共享某些资源的进程,它只是一种进程间共享资源的手段
  • 线程的创建和普通进程的创建类似,只不过需要传递一些参数标志来指明需要共享的资源(共享地址空间、文件系统资源、文件描述符和信号处理程序等)
  • 内核经常需要在后台执行一些操作,这就需要内核线程来完成,独立运行在内核空间的标准进程,内核线程和普通进程间的区别在于内核线程没有独立的地址空间(只在内核空间运行),可以被调度,可以被抢占
  • 内核线程只能由其它内核线程创建,内核是通过从kthreadd内核进程中衍生出所有新的内核线程的

5、进程终结

  • 进程终结时所需的清理工作(释放掉与进程相关联的所有唯一使用资源,进程不可运行,存在的唯一目的就是向它的父进程提供信息)和进程描述符的删除(释放占用的所有剩余资源)被分开执行
  • 如果父进程在子进程之前退出,成为孤儿进程就会在退出时永远处于僵死状态,解决办法是给子进程在当前线程组内找一个进程作为父进程,如果不行,就让init做它们的父进程
  • 子进程链表和ptrace子进程链表,当一个进程被跟踪时,它的临时父亲设定为调试进程,这时如果它父进程退出,系统会为它和它的所有兄弟重新找一个父进程,在一个单独的被ptrace跟踪的子进程链表中搜索相关的兄弟进程,用两个相对较小的链表减轻了遍历带来的消耗

进程调度

进程调度程序是在可运行进程之间分配有限的处理器时间资源的内核子系统,是多任务操作系统的基础,只有通过调度程序合理调度,系统资源才能最大限度地发挥作用,多进程才会有并发执行的效果。在一组处于可运行状态的进程中选择一个来执行,是调度程序所需完成的基本工作

1、多任务

  • 多任务操作系统就是能同时并发地交互执行多个进程的操作系统
  • 多任务系统划分为非抢占式多任务和抢占式多任务,Linux属于抢占式的多任务模式
  • 由调度程序来决定是什么时候停止一个进程的运行,以便其他进程能够得到执行机会,这个强制的挂起动作就叫做抢占
  • 进程时间片实际上就是分配给每个可运行进程的处理器时间段
  • 进程主动挂起自己的操作称为让步,让步机制让调度程序无法对每个进程该执行多长时间做出统一规定,进程独占处理器时间是无法预料的

2、调度策略

  • 进程分为I/O消耗型和处理器消耗型,调度策略通常需要在两个矛盾的目标中间寻找平衡,进程响应迅速,最大系统利用率,Linux更倾向于优先调度I/O消耗型进程
  • 调用算法中最基本的一类就是基于优先级的调度(根据进程的价值和其对处理器时间的需求来对进程分级的想法)
  • 调度程序总是选择时间片未用尽而且优先级最高的进程运行
  • Linux采用了两种不同的优先级范围
    • 1)用nice值, -20~19,越大优先级越低,值越小优先级越高,可以获得的处理器时间更多
    • 2)实时优先级,0~99,值越大优先级越高,任何实时进程的优先级都高于普通的进程

3、CFS调度算法(普通进程调度策略)

  • Linux调度器是以模块方式提供的,目的是容许不同类型的进程可以有针对性地选择调度算法,这种模块化结构被称为调度器类,它容许多种不同的可动态添加的调度算法并存,调度属于自己范畴的进程
  • 完全公平调度(CFS),CFS采用的方法是分配给进程一个处理器使用比重,完全摒弃时间片
  • CFS在所有可运行进程总数基础上计算出一个进程应该运行多久,nice值在CFS中被作为进程获取处理器运行比的权重,任何进程所获得的处理器时间是由它自己和其他所有可运行进程nice值的相对差值决定的
  • CFS为每个进程获取的时间片引入底线概念,这个底线称为最小粒度,默认1ms(可运行任务数趋于无限,有了最小粒度限制,就不能保证完全公平性了)

4、CFS调度实现

  • CFS调度实现的四个组成部分:时间记账、进程选择、调度器入口、睡眠和唤醒
  • 时间记账:需要确保每个进程只在公平分配给它的处理器时间内运行:
    • 1)虚拟运行时间(ns) 与定时器节拍不相关,记录一个程序到底运行了多长时间以及它还应该再运行多久
    • 2)进程选择,选择具有最小虚拟运行时间的任务,采用红黑树(一个自平衡二叉搜索树)组织查找
    • 3)调度器入口,入口点函数schedule(),以优先级为序,从高到低,依次检查每一个调度类,并且从最高优先级的调度类中,选择最高优先级的进程
    • 4)睡眠和唤醒,睡眠过程:进程把自己标记为休眠状态,从可执行红黑树中移出,放入等待队列,然后调用schedule()选择和执行一个其他进程;唤醒过程:进程被设置为可执行状态,然后再从等待队列中移到可执行红黑树中。(存在虚假唤醒情况,需要判断等待条件是否真正达成)

5、抢占和上下文切换

  • 上下文切换,就是从一个可执行进程切换到另一个可执行进程
  • 每个进程都包含一个need_resched标志,表明是否需要重新执行一次调度
  • 用户抢占,内核即将返回用户空间的时候,如果need_resched标志被设置,会导致schedule()被调用,此时就会发生用户抢占
  • 用户抢占发生情况:从系统调用返回用户空间时;从中断处理程序返回用户空间时
  • 内核抢占,Linux完整地支持内核抢占,只要重新调度是安全的,内核就可以在任何时间抢占正在执行的任务,只要没有持有锁,内核就可以进行抢占(有些内核代码需要设置容许或禁止内核抢占)
  • 内核抢占发生情况:中断处理程序正在执行,且返回内核空间之前;内核代码再一次具有可抢占性的时候;如果内核中的任务显示地调用schedule();如果内核中的任务阻塞

6、实时调度策略
Linux提供了两种实时调度策略:SCHED_FIFO、SCHED_RR,普通调度策略是SCHED_NORMAL,被一个特殊的实时调度器管理

  • SCHED_FIFO实现了一种简单的先入先出的调度算法,一旦处于可执行状态,就会一直执行,直到它自己受阻塞或显示地释放处理器为止,具有更高优先级的SCHED_FIFO、SCHED_RR才能抢占,同级轮流执行,只要有SCHED_FIFO级进程执行,其它低级别进程就只能等待
  • SCHED_RR级进程在耗尽事先分配给的时间后就不能再继续执行了,是一种实时轮流调度算法。底优先级进程不能抢占SCHED_RR任务,即便它的时间片耗尽
  • 这两种实时算法都是静态优先级,不为实时进程计算动态优先级,保证给定优先级别的实时进程总能抢占优先级比它底的进程
  • 默认情况下,nice值直接对应从100~139的实时优先级范围,实时进程总比普通进程优先执行

系统调用

系统调用在用户空间进程和硬件设备之间添加了一个中间层,其作用:

  • 为用户空间提供了一种硬件的抽象接口
  • 系统调用保证了系统的稳定和安全
  • 每个进程都运行在虚拟系统中,而在用户空间和系统的其余部分提供这样一层公共接口进行访问

1、与内核通信
在Linux中,系统调用是用户空间访问内核的唯一手段,除异常和陷入外,它们是内核唯一的合法入口

一般情况下,应用程序通过在用户空间实现的应用编程接口(API)而不是直接通过系统调用来编程(封装系统调用)

内核只跟系统调用打交道,库函数及应用程序怎么使用系统调用,不是内核关心的事

2、系统调用
内核必须提供系统调用所系统完成的功能,但它完全可以按照自己预期的方式去实现,只要最后的结果正确就行,系统调用为了保证兼容性,在用户空间和内核空间有不同的返回值类型

  • 系统调用号 在Linux中,每个系统调用被赋予一个系统调用号,用于关联系统调用,进程不会提及系统调用的名称,系统调用号一旦分配就不能再有任何变更,如果一个系统调用被删除,它所占用的系统调用号也不容许被回收利用
  • 系统调用性能 Linux系统调用比其它操作系统执行的快很多,很短的上下文切换时间是一个重要原因,另一个原因是系统调用处理程序和每个系统调用本身也很简洁

3、系统调用处理程序
用户空间的程序无法直接执行内核代码,应用程序靠软中断(中断号128)的方式通知系统:通过引发一个异常来促使系统切换到内核态去执行异常处理程序(此时即系统调用处理程序)

  • 指定恰当的系统调用 系统调用陷入内核方式一样,需要把系统调用号通过eax寄存器传递给内核
  • 参数传递 ebx ecx edx esi edi按照顺序存放前五个参数,用一个单独的寄存器存放指向所有这些参数在用户空间地址的指针,给用户空间的返回值也通过寄存器传递

4、系统调用的实现

  • 新增一个系统调用的时候,要时刻注意可移植性和健壮性
  • 系统调用必须仔细检查它们所有的参数是否合法有效

5、系统调用上下文
内核在执行系统调用的时候处于进程上下文,在进程上下文中,内核可以休眠并且可以被抢占(需要保证系统调用是可重入的)

6、注册成为系统调用

  • 在系统调用表的最后加入一个表项
  • 对于所支持的各种体系结构,系统调用号都必须定义
  • 系统调用必须被编译进内核映象(不能被编译成模块)
  • Linux提供了一组宏,用于直接对系统调用进行访问,它会设置好寄存器并调用陷入指令

7、尽量不通过系统调用实现
系统调用优点

  • 系统调用创建容易且方便
  • Linux系统调用有高性能

缺点

  • 需要官方分配系统调用号
  • 系统调用加入稳定内核固化
  • 系统调用需要注册到每个支持的体系结构中
  • 在脚本找那个不容易调用系统调用,也不能从文件系统直接访问
  • 难以维护主内核树之外的系统调用

新系统调用增加频率很低反映出Linux是一个相对较为稳定并且功能较为完善的操作系统

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值