一、基本概念
**书本上的概念:**程序的一个执行实例,正在执行的程序等
**基于内核的观点:**担当分配系统资源(CPU时间,内存)的实体。
我们知道,我们在写代码的时候,你的代码进行编译链接后生成可执行文件,这个文件就在磁盘当中,当我们双击这个文件之后,该文件就被加载到了内存之中,因为只有加载到了内存之中才能被cpu逐语句执行。而加载了内存中的程序,不再是程序而应该是进程。

二、描述进程------PCB
实际上我们在系统当中有很多的进程,我们可以通过ps aux命令来查看:

我们知道操作系统是第一个被加载到内存的,而操作系统就是做的管理工作的,那么操作系统是怎么做到管理的呢?
这里就用到了之前在谈操作系统时候提到的六个字:先描述,再组织。操作系统作为管理者,是不需要与进程直接交互的,当进程到来时,操作系统需要对进程进行描述,那么对进程的管理就变成了对描述的管理。进程的描述信息会被放到一个叫进程描述块之中,官方称之为PCB(process control block)。
操作系统将每一个进程都进行描述,形成了一个个的进程控制块(PCB),并将这些PCB以双链表的形式组织起来:

这样一来,操作系统只要拿到这个双链表的头指针,便可以访问到所有的PCB。此后,操作系统对各个进程的管理就变成了对这条双链表的一系列操作。
例如创建一个进程实际上就是先将该进程的代码和数据加载到内存,紧接着操作系统对该进程进行描述形成对应的PCB,并将这个PCB插到该双链表当中。而退出一个进程实际上就是先将该进程的PCB从该双链表当中删除,然后操作系统再将内存当中属于该进程的代码和数据进行释放或是置为无效。
总的来说,操作系统对进程的管理实际上就变成了对该双链表的增、删、查、改等操作。
1.task_struct------PCB的一种
因为Linux是拿C语言写的,那么这里的task_struct其实是一个结构体。
1)在Linux中描述进程的结构体叫做task_struct。
2)task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息。
2.task_struct的内容分类
主要内容:
- **标示符:**描述本进程的唯一标示符,用来区别其他进程
- **状态:**任务状态,退出代码,退出信号等。
- **优先级:**相对于其他进程的优先级。
- **程序计数器:**程序中即将被执行的下一条指令的地址。
- **内存指针:**包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- **上下文数据:**进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- **1/0状态信息:**包括显示的I/0请求,分配给进程的!/0设备和被进程使用的文件列表。
- **记账信息:**可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
三、查看进程
1.通过系统目录查看
我们除了上面的,通过ps aux命令查看以外,还可以在根目录下找系统文件夹proc。

我们打开文件夹,可以看到一些以数字命名的目录

这些数字就是我们之前所说的PID,在对应的目录中记录了进程的相关信息,比如我们查看1的文件内容:

2.通过ps命令查看
单独使用ps:
bash
ps aux

ps结合grep可以看到更加标准的进程信息:
四、通过系统调用获取进程的PID和PPID
这里我们要用到两个调用函数,分别是getpid()和getppid()来分别获取PID和PPID。
我们可以写个代码来测试一下:

输出:

我们也可以看一看进程的信息是不是和我们调用函数获取的一致

五、通过系统调用创建进程
1.fork函数创建子进程
fork函数是一个系统调用函数,他可以创建一个子进程:
例如:

运行结果如下:
运行结果中的第一行数据是该进程的PID和PPID,第二行是、fork函数创建的PID和PPID,不难发现fork函数创建的进程的PPID就是proc的PID,也就是说proc进程和fork创建的进程是父子关系。
没出现一个进程,操作系统都会为其创建PCB,fork也不例外。

我们知道加载到内存的代码和数据是父进程的,那么fork创建的子进程的代码和数据是哪里来的呢?
我们来写个代码来看看:

运行结果:

实际上,fork函数创建子进程,在fork函数之前的代码要被父进程执行,而fork()之后的代码默认是父子进程都可以执行的。
敲黑板:
1)这里虽然是父子进程代码共享,但是父子进程各自开辟空间(写时拷贝)。
2)这里面可能大家都会有一个疑问,那就是这里父子进程的执行顺序是什么样的,其实这里执行的顺序完全是不确定的,取决于操作系统的调度。
2.使用if来引出问题
我们在之前说了,在fork()函数之后的父子进程共享代码,那么这样一来,我们创建子进程就没有了意义,实际上使用的时候是要使用if来分流的,也就是父子进程去做不同的事情。
fork的返回值:
1.如果子进程创建成功了,在父进程中返回子进程的PID,而在子进程中返回0。
2.如果子进程创建失败,则父进程中返回。
既然子进程创建的返回值不一样,那么我们就可以通过这个性质来分流。
代码如下:

运行结果:

六、Linux的进程状态
进程从创建开始到被系统清理消亡的这个时间里,有时会占用CPU,有时在等待CPU分配资源,从这里看,进程是不同于程序的,进程有着自己动态变化的状态。下面的这个图就是我们待会要说明的。

我们可以看看在Linux中的进程状态的内容,下面是Linux内核的部分源代码:
cpp
/*
*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 *const task_state_array[] = {
"R (running)", /*0 */
"S (sleeping)", /*1 */
"D (disk sleep)", /*2 */
"T (stopped)", /*4 */
"t (tracing stop)", /*8 */
"X (dead)", /*16 */
"Z (zombie)", /*32 */
};
敲黑板:
这里的进程状态实际上是保存在进程控制块(PCB)中的,而在Linux中就是保存在了task_struct中的。
在Linux中我们可以使用ps aux和ps axj来查看进程的状态。
bash
ps aux

bash
ps axj

1.运行状态------R
这个状态(Runing)不一定就是进程处在运行当中,也有可能是在运行队列当中,也就是说系统中可以同时有多个处于R状态的进程。
敲黑板:
所有处于这个状态的进程,都是可以被调度的,他们在运行队列当中,在操作系统需要切换进程时,可以在这个里面直接选取。
2.浅度睡眠状态------S
一个进程处于浅度睡眠状态(sleeping),意味着该进程在等待事件的完成,这时的进程是可以被随时唤醒的(和人一样),也可以被杀亖(由于这个原因,这个状态也被叫做可中断睡眠(interruptible sleep)。
我们可以写个代码来演示一下:

这里我们让程序休眠100秒来模拟进程处于浅度睡眠状态。
bash
ps aux | head -1 && ps aux | grep test | grep -v grep

之前说过处于这个状态的进程是可以被杀亖的,我们来看看:

3.深度睡眠状态------D
一个进程处于深度睡眠状态(disk sleep),表示这个进程是不可以被杀亖的,即使是操作系统也不行,只能该进程自动唤醒才可以恢复。所以这个状态也可以被叫做是不可中断睡眠(uninterruptible sleep),处在这个状态的进程通常需要等待IO的结束。
其实我们通过他的英文也可以知道,他肯定适合磁盘(disk)相关的,具体的场景就是要向硬盘中写数据,这个过程是不能被杀亖的,因为我们等待磁盘的回复(是否写入完毕)来做出反应(磁盘处于休眠状态)。
4.暂停状态------T
在Linux中我们可以发送SIGSTOP信号让进程处于暂停状态(stopped),发送SIGCONT信号让处于暂停状态的进程重新运行起来。
例如:
我们发送一个SIGSTOP信号给test让进程处于暂停状态。

然后我们再发送一个SIGCONT信号让进程重新运行,这里运行的时候尽量快一点,因为时间一代程序就结束了:
补充说明:
我们可以使用kill -l命令来列出命令集
bash
kill -l

5.僵尸状态------Z
一个进程在要退出时,在系统层面,并不是我们想的直接就释放资源,而是会保存一段时间,来供操作系统或父进程读取退出信息,如果没有读取到相关的退出信息,那么数据也不会被释放,一个进程在等待数据被释放的过程就是处于僵尸状态(zombie)。
我们通过上面的描述也能知道僵尸状态其实是很必要的,因为进程就是去被指示去做事情的,那么指示方就应该要知道被指示方的完成情况,而僵尸状态就是指示方来获取完成情况的。
例如:我们之前一直在写的return 0;实际上这个0就是返回给给操作系统的,让操作系统知道我们完成的情况。在Linux中我们可以通过打印$?来获取最近一个进程的退出码。
bash
echo $?

敲黑板:
和运行状态一样,这个退出码也是被保存在PCB中的,在Linux中就是在task_struct中。
6.死亡状态
死亡状态就是一个理论上的返回状态,当一个进程的退出信息被读取后,该进程申请的资源就被释放掉了,那么该进程也就不存在了,所以我们不可能在我们列出来的信息中看到死亡状态。