文章目录
- 进程控制块的引入
- 初识进程控制块(PCB - Process Control Block)
- 什么是PCB
- Linux下的PCB
- 初见进程
- ps指令查看进程
- kill指令挂掉进程
- 通过系统调用接口得到进程的ID(进程标识符)
- 从根目录下的proc文件查看进程
- 通过fork函数创建子进程
进程控制块的引入
首先,听到进程这个词肯定不模糊,
因为我们常用的Windows系统就有进程管理器,
可以用进程管理器随时挂掉一个进程。
但是,进程和我们常说的程序有什么区别呢?
很多地方都写到,进程狭义的定义就是加载到内存中运行起来的程序:
但真的这么简单吗?
程序中的数据会一五一十地照搬到内存中吗?
照搬到内存之后操作系统又是怎么对零散的数据和代码进行处理呢?
我同时运行十几个程序,
这么多零散的代码和数据操作系统又是怎么能精准管理的呢?
这就引入了一个非常重要的概念 —— 进程控制块(PCB - Process Control Block)。
初识进程控制块(PCB - Process Control Block)
什么是PCB
为什么要有进程控制块,
很简单,因为操作系统要管理进程,
要管理就要先描述再组织,
进程控制块就是为了描述进程用的。
就好比我们用C 语言写一个简单的学生管理系统,
是不是也要声明一个结构体来描述一个学生呢?
Linux是用C语言写的,
所以Linux下的进程控制块就是用C语言声明的结构体,
它就是task_struct:
struct task_struct
{
///与进程相关的所有属性
}
这样操作系统就给每一份进程分配了一个PCB,
只需要将各个进程的PCB组织在一起,
对进程的管理就变成了对组织PCB的数据结构的管理。
首先先明确一下,一个进程最基本的要有哪些属性呢?
程序的代码和数据要加载到内存中,
要记录这些代码和数据的地址吧;
每个进程要区分一下吧,
要给每个进程编号并记录一下吧;
进程是在等待,还是在运行,还是在挂起,
要记录进程的状态吧;
这个进程是先执行还是后执行,
进程的优先级要记录吧;
进程执行到了哪里,下一条代码该执行什么,
这个也需要记录吧…
各种相关的属性集合在一起,加上程序本身加载到内存中的代码和数据,
就构成了一个完整的进程。
注意,这些属性在程序的代码和数据中包含吗?
对于大部分程序来说,答案肯定是否定的,
一个硬盘中的死程序怎么知道它的状态呢。
所以PCB的创建和组织是由操作系统来完成的。
同样地,PCB实际上是操作系统内核提供的结构,
所以一个完整的进程是什么呢,
是内核数据结构 + 代码和数据。
Linux下的PCB
上面提到,Linux下的PCB叫task_struct的结构体,
那这个结构体存放了进程的什么信息呢?
标示符: 描述本进程的唯一标示符,用来区别其他进程。
这个好理解,就好比学生管理系统中的每个学生都有一个学号来进行唯一标识,这里的标识符具体就是PID。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
所以就可以通过内存指针找到进程对应的数据和代码。
上下文数据: 进程执行时处理器的寄存器中的数据。
一个进程在执行的过程中难免会产生不少临时数据,而进程不是执行完这个就去执行下一个的,是并发运行的,因为对于优先级相同的进程要有相同的调度,不然相同优先级又何来相统一说呢?就比如A进程执行了10ms,执行完之后它再去后面排队,执行它后面的B进程,执行完10ms之后B进程跑到后面排队,依次往下来......既然进程不是一次就执行完的,它产生的临时数据也是要记录的,所以上下文数据其实就是指向的这一部分。
I/ O状态信息: 包括显示的I/O请求,分配给进程的I/ O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
其他信息
需要注意,task_struct中的内容远不止于此,
这里只是挑重点的浅浅认识了一下,
如果有兴趣的话可以参考一下大佬的博客:Linux进程管理之task_struct结构体 - zxiaocheng - 博客园 (cnblogs.com)
初见进程
既然对进程有一个大概的轮廓了,
怎么查看一个进程呢?
ps指令查看进程
Linux下可以通过一个简单的指令来查看:
ps axj
,用于查看所有进程的信息:
当然也可以通过grep命令加管道来查看一些具体的信息,
比如我写了一个C语言代码proc.c,
将其编译形成一个可执行程序proc,
程序运行起来成了进程,
就可以用下面的这条命令来查看:
ps axj | grep "proc"
当然如果想显示各个属性的名称还可以再加一句:
ps axj | head -1 & ps axj | grep "grep"
其中head -1
是提取文本的第一行,
也就是各个属性的名称:
其中PPID是父进程的ID
PID是当前进程的ID
TTY是进程运行的终端
STAT是当前进程的状态
UID是当前用户的ID
进程在运行的过程中就有了好多属性,很显然这些属性不是一成不变的,
所以进程在调度运行的时候,就具有了动态属性。
kill指令挂掉进程
我写的这个代码是一个死循环。
如果是前台运行,可以直接给它用Ctrl C
挂掉,
当然,拿到它的PID之后,可以直接用kill命令挂掉:
kill -9 PID
kill命令的好多选项如下,这里不做解释:
通过系统调用接口得到进程的ID(进程标识符)
我们也可以通过系统调用接口在程序内部查看进程的PID(getpid
)或PPID(getppid
):
相应的还有PPID,
PPID就是父进程的PID,没什么好说的,
不过需要注意,从命令行打开的进程,
它的父进程就是命令行bash:
从根目录下的proc文件查看进程
执行指令ls /proc
:
看到了好多数字命名的目录,
而这些目录其实就是进程的PID,
也就是说,一个进程的属性也是数据,
这个数据存在了内存中:
我们可以看到有一个exe文件,
它就指向了进程对应的硬盘上的可执行程序,
当进程挂掉之后,
对应的目录也就被清理掉了:
这里做个小实验;
当一个进程在运行的时候,我们把磁盘上对应的可执行程序删掉,这个进程还能正常运行吗?
可以看到,硬盘中的可执行程序删掉之后进程还在运行,
只是进程对应的目录下的exe指向变成了红色并标注了deleted。
所以对于部分进程,
并不依赖硬盘中对应的可执行程序,
当然这个结论并不严谨,
具体问题还需具体分析。
通过fork函数创建子进程
首先认识一下fork函数:
NAME
fork - create a child process
SYNOPSIS
#include <unistd.h>
pid_t fork(void);
RETURN VALUE
On success, the PID of the child process is returned in the parent, and 0 is returned in the child. On failure, -1 is returned in the parent, no child process is created, and errno is set appropriately.
这里只是浅浅地见识一下,关于具体细节不做讨论。
fork 如果成功创建子进程的话,
会给子进程返回0,给父进程返回子进程的PID。
如果创建失败的话就给父进程返回-1。
那么看下面的代码:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t id = fork();
if (id == 0)
{
printf("我是子进程, pid:%d, ppid:%d\n", getpid(), getppid());
sleep(1);
}
else if (id < 0)
{
perror("fork");
sleep(1);
}
else
{
printf("我是父进程, pid:%d, ppid:%d\n", getpid(), getppid());
}
return 0;
}
运行结果如下;
简单理解一下,子进程创建成功后,
会和父进程一样执行后续的代码,
数据在不修改的情况下会共用。
所以 fork 之后就有了两个进程,
两个进程的 id 不同,
但都会执行下面的 if-else 语句,
所以就有了不同的结果。
至于为什么会这样,不是这里的重点,
这里只是浅浅地认识一下。