文章目录
- 一、进程的概念
- 1. 进程是什么及进程的管理
- 2. Linux 下的 pcb
- 3. 系统调用接口 getpid 和 getppid
- 4. 系统调用接口 fork
一、进程的概念
1. 进程是什么及进程的管理
在 Linux下 ./binaryfile 运行一个程序或者在 Windows下双击运行一个程序时,程序就变成了一个进程
根据冯诺依曼体系结构,程序运行起来后,程序的代码和数据就会被操作系统加载到内存,但一个程序仅仅被加载到内存,并不代表该程序就是进程
将这里的内存比作学校,程序比作人,进程比作学校的学生,如果一个人处在学校中,并不能说明这个人就是学校的人,如:学校的保安,食堂阿姨等这些并不是学生,只有那些被学校管理起来的,并且信息在学校的学生档案中的人才能被称作学生,所以只有程序被操作系统管理起来,并且程序的代码和数据的相关信息被操作系统记录了,这个程序才被称做进程
在学校中,当学生很多时,我们需要对学生进行管理,在操作系统中,当很多程序运行起来时,加载到内存中的代码便会很多,操作系统便需要对这些代码和数据进行管理
根据我们对管理进行的建模,可以知道操作系统对加载到内存中代码和数据的管理方式:先描述,在组织
-
先描述:为了管理程序运行后加载到内存中的代码和数据,操作系统采用了一个结构体对象 pcb,用来描述加载到内存中的代码和数据的相关属性,其中 pcb 中有一个内存指针,用来指向内存中的代码和数据
在国内的教材中 pcb(process control block) 统一称作进程,在国外有的叫做任务,Linux 下的 pcb 称做 task_struck
进程 = 内核中关于程序的相关结构体 + 程序的代码和数据 -
在组织:每一个加载到内存中的代码和数据操作系统都会为其创建一个 pcb 对象,因此我们可以在 pcb 对象中在加上 pcb 结构体指针,构成数据结构中的链表
Linux 下采用双链表的形式组织
当操作系统想新增一个进程时(启动一个程序),只需要创建一个 pcb,然后录入该程序的属性到 pcb 中,然后在链表中插入该 pcb
当操作系统想杀掉一个进程时(结束程序的运行),只需遍历链表,找到该进程的 pcb,然后通过内存指针释放 pcb 指向的内存中的代码和数据,在再链表中释放该 pcb 结点即可
当操作系统想查看一个进程的运行状态时(查看程序运行是否正常),只需要遍历链表,找到该进程的 pcb,然后查看状态信息即可
当操作系统想找到一个优先级别较高的进程执行时(让 CPU 运行指定程序),只需要遍历链表,找到该进程的 pcb,然后通过内存指针找到 pcb 指向的代码和数据,让 CPU 执行即可
通过先描述,再组织的方式,操作系统对进程的管理被完全的转换成了对 pcb 结构体组成的链表数据结构的增删查改
运行的可执行程序都要被操作系统转换为进程来调度以便完成特定的任务,因此当我们运行一个程序时,就称作 创建了一个进程
仅个人当前理解:软件其实就是一个在磁盘上的二进制文件,当软件运行起来后,便需要加载到内存,所以操作系统对进程的管理,便是对软件资源的管理
进程 = 内核中关于进程的相关数据结构 + 进程的代码和数据
2. Linux 下的 pcb
为了操作系统管理进程,需要描述出进程的共同属性,从而产生了结构体 pcb
task_struct 的字段
- 标识符:描述本进程的唯一标识符,用来区别进程
- 状态:任务状态,退出代码,退出信号等
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令的地址
- 内存指针:包括程序diamante和进程的相关数据结构的指针,还有和其他进程共享的内存块的指针
- …
pcb 和可执行程序的文件属性关系不大
3. 系统调用接口 getpid 和 getppid
如何证明程序运行起来,便成为了一个进程?
当我们创建一个进程时,操作系统就会在 /proc 目录下创建一个该进程 pid 为名的目录,该目录下存在该进程的属性(文件路径等),当进程终止时,/proc 目录中也会删除该进程 pid 为名的目录
预备知识1:
ps ajx:查看系统中所有的进程
ls /proc:查看系统中的所有进程,其中目录名为数字的表示进程的标识符 pid
[starrycat@iZ2vcer6gtjgqa43cdpeeaZ code]$ ps ajx
PPID PID PGID SID TTY TPGID STAT UID TIME COMMAND
0 1 1 1 ? -1 Ss 0 3:57 /usr/lib/systemd/systemd --system --deserialize 17
0 2 0 0 ? -1 S 0 0:00 [kthreadd]
2 3 0 0 ? -1 S 0 0:08 [ksoftirqd/0]
2 5 0 0 ? -1 S< 0 0:00 [kworker/0:0H]
2 7 0 0 ? -1 S 0 0:00 [migration/0]
...
[starrycat@iZ2vcer6gtjgqa43cdpeeaZ code]$ ls /proc
1 14 21134 25405 28 350 47 539 6770 854 crypto interrupts kpagecount mtrr softirqs uptime
10 1523 21252 25512 280 36 49 557 6771 9 devices iomem kpageflags net stat version
101 15631 21262 26 29 365 5 587 7 acpi diskstats ioports loadavg pagetypeinfo swaps vmallocinfo
1019 16 22 27 296 37 50 598 787 buddyinfo dma irq locks partitions sys vmstat
...
预备知识2:
pid_t getpid(void):返回调用该函数的进程标识符 pid,需要包含头文件 <sys/types.h> 和 <unistd.h>
pid_t:有符号整形的 typedef
如果想了解更多关于 getpid 函数的内容,通过 man 2 getpid 即可查看
接下来通过代码证明程序运行起来,便成为了一个进程
在 process.c 中写好如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("我是一个进程了,我的 pid 是:%d\n", getpid());
sleep(1);
}
return 0;
}
证明1:
打开新的窗口,通过命令查看发现,运行的程序 process 可以查到他的进程标识符 pid
其中 ps ajx | head -1 表示获取进程的第一行属性字段(便于观看)
&& 表示当前一条指令执行成功后执行后一条指令
ps ajx | grep process | grep -v grep 表示筛选出 process 的进程,并且去除掉 grep 这个进程
过程2:
用 ctrl + c 终止程序后,便查找不到该进程了
终上所述,程序运行起来后就变成了进程
在进程中存在着父子进程的概念
pid_t getppid(void):返回调用该函数的进程的父进程标识符 pid,需要包含头文件 <sys/types.h> 和 <unistd.h>
pid_t:有符号整形的 typedef
在 process.c 中写好如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(1)
{
printf("我是一个进程了,我的 pid 是:%d,我的 ppid 是:%d\n", getpid(), getppid());
sleep(1);
}
return 0;
不断的执行 process 会发现子进程的进程标识符一直在增加,但是父进程的进程标识符 6771 一直不变,通过 ps 命令后可以发现 6771 这个进程标识符就是命令行解释器 bash,即命令行执行的程序都是通过创建子进程的方式去执行的,是为了避免执行的程序挂了,导致影响 bash 自己
命令行解释器 bash 其实就是在 /bin/bash 的一个二进制可执行程序,因此 bash 也是一个进程
命令行启动的所有程序,都是 bash 创建的子进程,为了防止子进程挂了,导致影响自己
4. 系统调用接口 fork
命令行是如何创建子进程的呢?
pid_t fork(void):如果创建子进程成功,则给调用该函数的父进程返回子进程的 pid,给子进程返回 0,如果失败则返回 -1,需要包含头文件 <sys/types.h> 和 <unistd.h>
- 在代码中执行到 fork 语句后,执行流变成了两个执行流
- fork 之后的代码,父子进程都会执行,因此我们可以用 if 语句来让执行流分流,以便父子进程执行不同的代码块
在 process.c 中写好如下代码:
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t ret = fork();
if (ret == 0)
{
// 执行子进程
while (1)
{
printf("我是子进程,我的 pid 是:%d,我的 ppid 是:%d\n", getpid(), getppid());
sleep(1);
}
}
else if (ret > 0)
{
// 执行父进程
while (1)
{
printf("我是父进程,我的 pid 是:%d,我的 ppid 是:%d\n", getpid(), getppid());
sleep(1);
}
}
else
{}
return 0;
}
- fork 创建子进程成功之后,父进程和子进程谁先被 CPU 调度运行是由操作系统决定的
可以通过 fork 系统调用来创建子进程
fork 创建子进程的过程:操作系统为了可以管理子进程,会根据父进程的 pcb 创建子进程的 pcb,并且子进程的 pcb 会和父进程的 pcb 指向同一块代码和数据
虽然父进程创建子进程后,子进程指向父进程的代码和数据,但是不同的进程在运行时是独立的,父子进程在运行时也是独立的
kill -9 pid 功能:杀掉进程
再次运行 process 之后,在另一个窗口中输入 kill -9 父进程 pid,此时子进程任然可以正常运行
父子进程是如何做到独立的呢?
- 在代码层面:可执行程序的代码都是二进制机器指令了,是只读的,不可能被修改,因此父子进程可以一起读代码,只需要记住自己进程执行代码的位置即可
- 在数据层面:Linux 操作系统采用写时拷贝的方式来保证数据的独立性,即:当某一个进程想要修改数据时,操作系统会自己拷贝一份数据到别的位置然后进行修改
为什么一个函数会有两个返回值?
因为一个函数在执行 return 之前,函数的主题功能已经完成了,对于 fork 函数,在 return 之前已经创建好子进程了,此时便有父子进程两个执行流,于是父子进程都会执行 return 语句,也就产生了两个返回值的现象