基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
直接看上面的基本概念显得很抽象,那我们就先不管它。先来粗略地看看在windows
系统下的进程:
通过任务管理器,我们可以看到一个个正在运行的程序,这就代表了一个个进程。 我们可以直观地感受到:在操作系统中,进程可以同时存在很多个
描述进程-PCB
既然操作系统中可以有很多进程,那操作系统就要将这些进程管理起来,操作系统是如何管理进程的呢?
先描述,再组织。想了解如何管理进程,我们就要先将它描述出来
我们先来写一个简单程序,然后将其编译为可执行程序myprocess
#include <stdio.h>
#include <unistd.h>
int main()
{
while(1)
{
sleep(1);
printf("I am a process!\n");
}
return 0;
}
当我们的程序被CPU调度运行时,就是一个进程了
当程序没有运行时,它是存储在磁盘中的
根据冯诺依曼体系规定,我们要运行程序,必须将其加载到内存中,也就是要将程序对应的代码与数据拷贝到内存中
那么像上图这样的模型,将代码与数据加载到内存中就是一个进程了吗?
我们知道,内存中可以存在多个进程,哪个进程该调度,调度多长时间,下一个进程又是谁,这些问题似乎仅凭上图的模型无法解决,这里我们就要引入一个数据结构——PCB(Process Control Block)进程控制块
PCB中存放着进程的信息,可以理解为进程属性的集合
它的具体实现是怎么样的,我们现在先不去深究,只需了解以下即可
struct PCB
{
// ...所有属性
struct PCB* next // 指向下一个进程的PCB
// 内存指针, 指向对应的代码与数据
}
每一个进程都必须对应一个PCB
这样我们就可以粗略的认识,进程 = PCB + 对应的代码与数据
有了PCB
,操作系统对进程的管理,就变为了对PCB
链表的增删查改,当然也不一定是链表,也可能是其他的数据结构
PCB
是在操作系统学科里的一个统称,在Linux
中,PCB
具体叫法是struct task_struct
,是一种内核数据结构
task_ struct内容分类
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
PCB的意义
通过以上,我们了解了什么是PCB
,下面我们来看看它的意义或者作用
我们知道,操作系统也属于软件,在开机时就被加载到了内存中
而程序运行时,进程 = 内核task_struct
结构体 + 程序的代码与数据,都会被加载到内存
操作系统对进程进行调度时,就是调度进程对应的task_struct
,会有一个task_queue
或者其他数据结构,让不同进程的task_struct
进行排队
所以,调度运行进程,本质就是让进程控制块task_struct
进行排队
有了PCB,操作系统就可以对进程进行管理
进程标识符
到这里,我们知道,运行一个可执行程序,就是让系统创建进程并运行。
无论是我们写的代码形成的可执行文件,还是系统的指令(ls
、pwd
等)操作,本质都是运行进程
当进程运行起来时,我们可以使用如下指令查看进程运行的情况
ps axj
ps
是查看进程的指令,axj
是选项,选项可以乱序
直接使用,显示出来的数据很乱,通常要配合管道grep
来查看特定进程
ps axj | grep 进程名
下面运行之前的myprocess
,然后使用ps
指令查看此进程
可以看到,查到了我们想要的进程状态,但是我们并不知道这些数据代表什么,我们可以将ps axj
的前一行取出来,对应了各个数据的名称
ps axj | head -1
而且下面还有一行grep
进程,这是因为grep
运行时也会创建进程,我们可以将这一行过滤掉,grep -v grep
是反向过滤,将含grep
的数据过滤掉
ps axj | grep myprocess | grep -v grep
同时运行上面两条指令,可以这样
ps axj | head -1 && ps axj | grep myprocess | grep -v grep
这时我们再来看进程运行情况
先来看PID
这一项,为了区分不同进程,每个进程都有唯一标识符,就是PID
getpid
除了使用ps axj
指令,还可以怎样查看进程对应的PID
呢?——进程可以自己查看自己的PID
我们要明确一点,进程是运行起来的程序,程序是我们写的,我们代表了用户。而PID
是操作系统内核的数据,用户是不可以直接访问操作系统内部的,但是操作系统为用户提供了系统调用。因此,我们可以在程序中使用系统调用来查看进程的PID
。
这个系统调用是getpid
,头文件是<unistd.h>
和 <sys/types.h>
,返回类型是pid_t
无符号整型
下面我们直接使用
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = getpid();
while(1)
{
sleep(1);
printf("I am a process, pid:%d\n", pid);
}
return 0;
}
将编译后的程序运行起来:
通过getpid
得到的pid
和我们使用ps
命令得到的pid
是一致的
由于我们写的程序是个死循环,要想停止只能按CTRL+C
,而这里有了进程的pid后,可以使用kill
指令停止进程
kill -9 进程的pid
进程的创建
除了直接运行可执行程序,我们还可以在代码中创建进程
进程创建的代码方式
首先,我们要知道,一个进程是由其父进程创建的
使用系统调用getppid
可以获得当前进程的父进程的pid
,先来使用一下看看
运行程序:
确实获得了一个ppid
,多运行几次程序
我们发现,pid
每次都会改变,而它父进程对应的ppid
是不变的。这很容易理解,我们每次运行一个程序,都是重新创建一个新的进程,pid
肯定会不同,而它的父进程ppid
不变,说明父进程一直在运行着
我们使用ps
指令和查看一下父进程
可以看到,父进程是bash
,命令行解释器,说明我们运行的程序的进程是由父进程bash
创建的子进程
知道了进程是由父进程创建后,我们该怎么在代码中创建进程呢?
用户不能直接访问操作系统内核,我们想在代码中创建一个进程,只能使用系统调用
pid_t fork()
先不谈它的返回值,直接使用来看看
这里先来暂时输出一个结论:fork之后,父子进程代码共享。即fork
之后,会输出两条I am a process
,一个是父进程输出的,另一个是子进程输出的
我们知道,父进程是myprocess
,它的代码与数据来源于磁盘;它创建子进程,子进程的代码与数据来源于哪里?这里我们暂时理解为子进程继承了父进程的代码与数据
为什么要创建子进程
父进程与子进程运行不同的代码,可以提高工作效率
父子进程代码是共享的,如何运行不同的代码呢?这就要返回去看fork
的返回值了
fork
的返回值:
创建子进程成功,子进程的pid
被返回给父进程,返回0
给子进程;创建失败,返回-1
给父进程
我们根据fork
的返回值,来写一份代码:
运行结果:
可以看到因为fork
的返回值不同,父子进程也执行了不同的代码
回头看代码有两个问题:
- 为什么
fork
函数可以返回同时两个值 - 为什么
id
可以同时等于0
和子进程的pid
解答
fork
函数在执行的时候,子进程就创建了。在fork
函数执行结束,返回值时,同时有父子两进程,同时有两个fork
函数,所以可以返回两个值- 进程具有独立性,父子进程的数据互不影响。可以非常粗略地认为,父进程和子进程有自己变量
id
,是各自私有的,只是都叫id
而已。后面虚拟地址空间、写时拷贝会解开这个问题的答案
以文件的形式查看进程
每一个进程运行时,都会在/proc
目录下生成对应的文件
写一个简单的程序,在proc
中查看它的进程文件
运行起来:
可以看到,确实生成了对应进程的目录
看看里面的详细信息:
很明显地可以看到cwd
和exe
cwd(current work dir)
代表了当前进程当前的工作路径exe
记录了进程对应的可执行程序的路径
结束,再见 😄