文章目录
- 1. 基本概念
- 2. 进程的描述
- 3. 进程的一些基本操作
- 3.1 查看进程
- 3.2 结束进程
- 3.3 通过系统调用获取进程标示符
- 3.4 通过系统调用来创建子进程
- 4. 进程状态
- 4.1 操作系统的进程状态
- 4.2 Linux对于这些状态的处理方式
1. 基本概念
什么是进程?
在回答这个问题之前想问另一个问题:什么是程序?
- 我们写的代码(源文件)在经过编译器编译之后生成的是文件,那么就是存放在磁盘中的。所以程序的本质就是文件,在磁盘存放
所谓的进程,就是运行着的程序,我们知道程序的运行肯定是需要CPU做运算的,CPU只能直接与内存做交互,所以程序一定要加载到内存中才能运行(这是体系结构决定的!!)。
同一时间,内存中会有很多进程,所以我们就要这些进程**管理**起来。
那么,怎么管理呢?
2. 进程的描述
在上一篇文章中我们讲到,所谓的管理就是:先描述,再组织
我们一般会采用一个结构体来描述这些进程的相关属性
进程信息被放在一个叫做进程控制块的数据结构中,可以理解为进程属性的集合。课本上称之为PCB(process control block), Linux操作系统下的PCB是: task_struct
上面是部分内核的代码截图,实际上内核中有关task_struct的代码有很多,主要可以分为以下几类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程;
- 状态: 任务状态,退出代码,退出信号等;
- 优先级: 相对于其他进程的优先级;
- 程序计数器: 程序中即将被执行的下一条指令的地址;
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针;
- 上下文数据: 进程执行时处理器的寄存器中的数据;
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表;
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等;
- 其他信息
抽象出来大概可以这样表示:(假设系统中进程使用链表的结构组织)
struct task_struct
{
//进程的所有属性
//...
//进程对应的代码和数据的地址
//...
//下一个进程的地址
struct task_struct* next;
};
关于task_strcut更详细的讲解,可以看一看这一篇博客:Linux中进程控制块PCB-------task_struct结构体结构(童嫣
有了上述的观念之后,我们从内核的角度来看,进程就是**内核数据结构(task_struct) + 进程对应的磁盘代码**
3. 进程的一些基本操作
3.1 查看进程
- 方法一:
ps axj
可以查看当前所有的进程
将ps axj
结合管道
和grep指令
可以显示我们想要看到的进程,同时想要看到所有信息的含义,就要加上head -1
表示将查询到的第一行数据也显示出来
- 可以在
/proc
目录下查看当前所有进程
3.2 结束进程
对于一般进程,我们可以使用**[Ctrl + c]结束,也可以使用kill**命令结束
3.3 通过系统调用获取进程标示符
每个进程都有一个唯一标识符:PID我们可以通过系统调getpid()
用来获取这个唯一标识符
注意:
-
这里的getpid和getppid获取到的就是进程的标识符,其中
getpid
是获取进程的PID,getppid
是用于获取父进程PID -
函数的返回值pid_t实际上就是int,只是在系统层面进行了封装
可以看到,我们通过 getpid() 和 getppid() 函数得到的值的确是我们进程对应的id;同时,我们发现myproc进程的父进程是 bash,即 shell 外壳,这也侧面证实了我们之前提到的结论 – shell 为了防止自身崩溃,并不会自己去执行指令,而是会派生子进程去执行。
同一个程序重新被运行时它的进程id可能与之前不一样,因为它的代码和数据需要重新从磁盘中加载;但是它的父进程id一定是一样的,因为它们都是通过 bash 来执行
3.4 通过系统调用来创建子进程
我们可以通过系统调fork
来创建子进程
可以看到,所有子进程的id都是0,父进程的id都是子进程的pid,这是因为fork的返回值规定:当fork执行之后将会产生两个进程,其中父进程的返回值是子进程的pid,子进程的返回值是0,fork之后的所有代码都被父子进程共有
4. 进程状态
4.1 操作系统的进程状态
在真正进入的进程状态的学习之前,大家或多或少应该听说过一些进程的状态,比如:挂起,僵尸等等,在进入的进程状态的学习之前,我们需要统一一些观点:
-
状态是进程内部的属性,所有的属性都在PCB里
-
进程不只会占用CPU资源,也有可能随时要外设资源
-
运行状态
进程PCB在运行队列里面的进程状态就是运行状态(不是说进程在运行时才是运行状态)
运行队列的概念:
CPU需要对进程做执行,同一时间操作系统内会有很多的进程,那么这些进程谁先谁后呢?此时就需要一个队列将这些进程管理起来,确定谁先执行谁后执行。这个队列里面保存的实际上并不是每个进程对应的二进制代码,而是PCB(task_struct),这个队列就是运行队列(run queue)
每个CPU拥有一个运行队列
-
阻塞状态
我们在上面说到,进程不止会占用CPU资源,也可能随时需要外设资源,当一个进程在运行的过程中需要访问外设资源的时候,由于外设很慢(相对于CPU来说),所以进程需要等待外设,此时CPU就想去处理其他进程,那么当前进程就会进入到阻塞状态
拓展一下:
每个CPU执行一个进程,当前进程在处于阻塞状态,等待其他硬件资源的时候,实际上也就是当前进程的PCB进入到了其他硬件的run queue中,也就是说每一个硬件都拥有一个runqueue
-
挂起状态
如果系统中存在很多进程,当前进程短时间内不会被调度,代码和数据短时间内不会被执行,此时如果操作系统中的内存不够,就会将这些进程的相关信息保存在磁盘中,节省一部分内存。此时这些被保存在磁盘上的进程就是处于挂起状态
挂起一定是阻塞的,阻塞不一定挂起
4.2 Linux对于这些状态的处理方式
在上一小节中我们讲到了操作系统内进程的概念性状态,那么在Linux下的表现形式是怎样的呢?
在Linux源码里面能够找到一个数组:task_state_array
罗列了Linux下所有的进程状态
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char *task_state_array[] = {
"R (running)", /* 0 */ // 运行状态
"S (sleeping)", /* 1 */ // 阻塞状态
"D (disk sleep)", /* 2 */ // 磁盘休眠状态/深度睡眠状态(了解)
"T (stopped)", /* 4 */ // 暂停状态
"T (tracing stop)", /* 8 */ // 该进程正在被追踪(看到的是t状态)
"Z (zombie)", /* 16 */ // 僵尸状态
"X (dead)" /* 32 */ // 死亡状态
};
接下来我们来看一些进程状态的例子:
- 运行状态:
- 休眠状态
这里出现休眠状态的原因是:调用printf的时候需要访问外设——显示器,由于外设的速度很慢(相对于CPU而言),所以大部分时间都是在等在显示器资源,所以我们能看到的状态大多都是休眠。当然如果非常巧合的话,也是能看到R状态的
- 暂停状态
这里扩展一下:S是浅度睡眠,可以被终止;D是深度睡眠,无法被OS杀掉,只能通过断电、自己醒来进行解决。出现的原因一般是由于高IO导致的。
- t状态
tracing stop表示该进程正在被追踪
本节完