操作系统(一)——进程

前言

近期看了几本关于Linux的书籍,打算再重温一下APUE(《Unix环境高级编程》),需要写几篇文章沉淀一下之前看过的记录自己的理解。其实我对Linux系统(或者说操作系统)最感兴趣的部分还是如何执行程序,进程间如何进行调度(进程也成为任务,下文的伪代码中用task来表示跟进程相关的结构)。
这一系列的第一篇就写写自己对进程的理解吧,由于存在一些理解上的偏差,文章描述可能有误,还请指正。

进程初始化

我们告诉操作系统我们要执行程序A和程序B,于是操作系统从硬盘里边把程序A跟程序B读出来,并初始化,最终放入调度队列,整个初始化进程的流程如图1所示:

图1 进程初始化

在初始化进程的内容之前,需要把进程的状态先设置成TASK_UNINTERRUPTIBLE,表示这个进程还不能被唤醒跟不能被调度,在设置完之后把进程状态设置成就绪状态TASK_RUNNING,此时的进程就具备了运行的条件,但是具体能不能运行还得看之后的调度程序是否选择它来运行。

假设在程序A跟程序B运行之前,还有进程C在进程队列里边,于是此时的进程队列如下:

图2 进程队列

明明上边说了只有ABC三个进程,现在图中又多了进程0跟进程1,简单说明一下:

  1. 进程0是一个idle进程,内核的主函数,进行一些硬件初始化之后便创建它的一个子进程 “init进程”,随后进入死循环,一直在pause(),也即是最后进程0变成了空闲进程。
  2. 进程1是init进程,初始化根文件系统,执行配置的初始化命令,接着执行用户登录的shell程序,之后所有的进程其实都是init的后代进程了。

现在进程初始化的工作结束了,紧接着需要由我们的“上帝”(进程调度)来控制现在应该由哪个进程去运行。

进程调度

进程调度交由函数schedule()来处理,其主要的工作就是决定当前要运行哪个进程,直接上图:

图3 进程调度

其实调度的算法在操作系统的书籍上讲得太多了,完全理解各个调度算法就是另外需要探讨的东西了,我仅仅想记录一下这个调度的流程,而非细到具体的算法。
调度的工作其实就是:从就绪的进程中挑选出一个最合适的进程出来运行。
什么时候会处于就绪状态呢?我在这里先不讨论,但在调度前边,我们需要激活一些设置了定时器的进程,例如有个进程设置了2秒后执行run函数,于是乎在2s后(不一定是2s准时)的某个时间点,调度程序开始运行了,此时扫描了整个进程队列发现该进程已经倒数2s完毕,于是把这个进程的状态设置成就绪(其实同时还要设置信号,因为信号处理的回调跟原先被中断执行的地方是两个位置,具体就不细展开),让它接下来有机会被执行到,因此设置了定时器的进程其实不一定真的能够非常准时的执行定时器的回调(这个得看机器跟机器运行的情况了)。
从流程图看出这里涉及到一个简单粗暴的进程队列扫描操作,当然也可以有优化的地方,类似建多几个map分别指向不同状态的进程队列,为了让问题更简单,直接采用简单暴力的方法容易阅读一些。

进程调度的时机

这里还有个问题,假设现在CPU正在执行A进程,执行到printf的时候,CPU突然去执行调度程序把A挂起来,让CPU去执行B进程。CPU怎么知道什么时候要执行调度程序呢?
其实这里还有个中断的概念在,假设我们的CPU在指导A进行操作的时候,突然有个计时员来告诉CPU说,现在时间到了,你需要去指导一下别人怎么操作了。于是乎CPU就告诉A,我要去别的地方一会,晚些再回来指导你。这个计时员的工作非常重要,在计算机里边由一个定时芯片来充当此角色,操作系统会设置一个时间,例如1秒,那么计时员就会每隔1秒钟来提醒你该换个程序运行了,这个时候CPU就会停下现在的工作,把当前的进程的状态存起来,然后通过调度程序调出另一个符合条件的进程出来运行(也就是所谓的上下文切换),可以看到每次每个进程运行的时间片段不会超过1秒钟。

进程等待与唤醒

我们还存在这样的一个现象,例如我要锤钉子,现在我叫你帮忙去买个锤子回来,我只有等你买完锤子回来给我了,我才能继续我接下来的活。这里等同于”我锤钉子“这个进程先创建了一个子进程”你去买锤子“,父进程要等子进程结束之后,父进程才能继续进行。例子如下:

cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <stdio.h>
int main()
{
      printf("我缺一把锤子\n");
      int pid = fork();//在这里会创建子进程,相当于下边的代码被赋值多一份,然后分开运行了
 
      if (pid == 0)
     {//pid为0表示子进程
         printf("你去买个锤子\n"); 
 
         printf("你买完锤子回来了\n");
     }else
     {//否则父进程,这里忽略pid<0创建进程失败的情况
          waitpid(pid);//等待子进程的结束,父进程会在这被挂起等待
 
          printf("有锤子锤钉子了!\n");
     }
      return 0;

这里就涉及到一个等待跟唤醒的机制了,执行到waitpid的时候,CPU发现当前进程需要等待另一进程的结束才能运行,流程如下:

图4 进程等待

什么时候会唤醒父进程呢?也就是在子进程结束的时候会有 某种方式告诉父进程,也就是下边要写的”进程结束“的阶段。

进程结束

进程总有结束的那一刻,对于任何程序载入内存后,其运行的其实是:exit(main())
可以看到main函数执行完之后,其返回值会丢给exit函数,而exit函数会把当前这个进程从进程队列里边去掉,做一些销毁的操作(例如关掉打开的文件描述符),同时告诉相关的进程说我这个进程结束了!流程图如下:

图5 进程结束

后话

写这文章的时候,发现还是能重温到不少细节的地方,多写写文章还是有帮助的,甭管写的二不二,旨在沉淀跟分享,有误的地方还请指正。

本文链接:操作系统(一)——进程

转载声明:本博客文章若无特别说明,皆为原创,转载请注明来源:拉风的博客,谢谢!

发表评论

电子邮件地址不会被公开。


× 7 = 7

您也可以使用微博账号登陆