全方位剖析 Linux 操作系统,太全了( 九 )


在子进程开始运行后,操作系统会调用 exec 系统调用,内核会进行查找验证可执行文件,把参数和环境变量复制到内核,释放旧的地址空间 。
现在新的地址空间需要被创建和填充 。如果系统支持映射文件,就像 Unix 系统一样,那么新的页表就会创建,表明内存中没有任何页,除非所使用的页面是堆栈页,其地址空间由磁盘上的可执行文件支持 。新进程开始运行时,立刻会收到一个缺页异常(page fault),这会使具有代码的页面加载进入内存 。最后,参数和环境变量被复制到新的堆栈中,重置信号,寄存器全部清零 。新的命令开始运行 。
下面是一个示例,用户输出 ls,shell 会调用 fork 函数复制一个新进程,shell 进程会调用 exec 函数用可执行文件 ls 的内容覆盖它的内存 。

全方位剖析 Linux 操作系统,太全了

文章插图
 
Linux 线程现在我们来讨论一下 Linux 中的线程,线程是轻量级的进程,想必这句话你已经听过很多次了,轻量级体现在所有的进程切换都需要清除所有的表、进程间的共享信息也比较麻烦,一般来说通过管道或者共享内存,如果是 fork 函数后的父子进程则使用共享文件,然而线程切换不需要像进程一样具有昂贵的开销,而且线程通信起来也更方便 。线程分为两种:用户级线程和内核级线程
用户级线程用户级线程避免使用内核,通常,每个线程会显示调用开关,发送信号或者执行某种切换操作来放弃 CPU,同样,计时器可以强制进行开关,用户线程的切换速度通常比内核线程快很多 。在用户级别实现线程会有一个问题,即单个线程可能会垄断 CPU 时间片,导致其他线程无法执行从而 饿死 。如果执行一个 I/O 操作,那么 I/O 会阻塞,其他线程也无法运行 。
全方位剖析 Linux 操作系统,太全了

文章插图
 
一种解决方案是,一些用户级的线程包解决了这个问题 。可以使用时钟周期的监视器来控制第一时间时间片独占 。然后,一些库通过特殊的包装来解决系统调用的 I/O 阻塞问题,或者可以为非阻塞 I/O 编写任务 。
内核级线程内核级线程通常使用几个进程表在内核中实现,每个任务都会对应一个进程表 。在这种情况下,内核会在每个进程的时间片内调度每个线程 。
全方位剖析 Linux 操作系统,太全了

文章插图
 
所有能够阻塞的调用都会通过系统调用的方式来实现,当一个线程阻塞时,内核可以进行选择,是运行在同一个进程中的另一个线程(如果有就绪线程的话)还是运行一个另一个进程中的线程 。
从用户空间 -> 内核空间 -> 用户空间的开销比较大,但是线程初始化的时间损耗可以忽略不计 。这种实现的好处是由时钟决定线程切换时间,因此不太可能将时间片与任务中的其他线程占用时间绑定到一起 。同样,I/O 阻塞也不是问题 。
混合实现结合用户空间和内核空间的优点,设计人员采用了一种内核级线程的方式,然后将用户级线程与某些或者全部内核线程多路复用起来
全方位剖析 Linux 操作系统,太全了

文章插图
 
在这种模型中,编程人员可以自由控制用户线程和内核线程的数量,具有很大的灵活度 。采用这种方法,内核只识别内核级线程,并对其进行调度 。其中一些内核级线程会被多个用户级线程多路复用 。
Linux 调度下面我们来关注一下 Linux 系统的调度算法,首先需要认识到,Linux 系统的线程是内核线程,所以 Linux 系统是基于线程的,而不是基于进程的 。
为了进行调度,Linux 系统将线程分为三类
  • 实时先入先出
  • 实时轮询
  • 分时
实时先入先出线程具有最高优先级,它不会被其他线程所抢占,除非那是一个刚刚准备好的,拥有更高优先级的线程进入 。实时轮转线程与实时先入先出线程基本相同,只是每个实时轮转线程都有一个时间量,时间到了之后就可以被抢占 。如果多个实时线程准备完毕,那么每个线程运行它时间量所规定的时间,然后插入到实时轮转线程末尾 。
注意这个实时只是相对的,无法做到绝对的实时,因为线程的运行时间无法确定 。它们相对分时系统来说,更加具有实时性
Linux 系统会给每个线程分配一个 nice 值,这个值代表了优先级的概念 。nice 值默认值是 0,但是可以通过系统调用 nice 值来修改 。修改值的范围从 -20 - +19 。nice 值决定了线程的静态优先级 。一般系统管理员的 nice 值会比一般线程的优先级高,它的范围是 -20 - -1 。


推荐阅读