往期文章:《Linux:深入了解冯诺依曼结构与操作系统》
Linux:深入理解冯诺依曼结构与操作系统-CSDN博客
目录
1. 概念
2. 描述进程
3. 深入理解进程的本质
4. 进程PID
4.1 指令获取PID
4.2 geipid函数获取PID
4.3 kill指令终止进程
4.4 进程信息文件夹
(1)exe
(2)cwd
5. 进程PPID
5.1 getppid函数获取PPID
5.2 fork函数创建子进程
6. 创建多进程
7. fork函数如何返回两个值
1. 概念
进程(Process)是指一个正在执行的程序实例。在操作系统中,进程是资源分配和调度的基本单位。可执行程序与进程的区别:可执行程序是一组指令的集合,它存储在磁盘上,而进程则是程序在执行时的一个实例,它存在于内存中。
2. 描述进程
操作系统也称之为Operating System,缩写就是OS。
操作系统笼统的分为内核(kernel)和外壳程序。内核有进程管理、内存管理、文件管理和驱动管理。操作系统管理任何对象,都是对其属性进行管理,遵守先描述,再组织的原则。
因此,操作系统管理进程,相当于管理它的属性,会把进程属性放在一个叫做进程信息控制块的数据结构当中,就是进程属性的集合。这也称为PCB(process control block),Linux操作系统下的PCB是task_struct。
下面是task_struct结构体中包含的内容:
- 标示符: 描述本进程的唯一标示符,用来区别其他进程。
- 状态: 任务状态,退出代码,退出信号等。
- 优先级: 相对于其他进程的优先级。
- 程序计数器: 程序中即将被执行的下一条指令的地址。
- 内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
- 上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
- I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
- 其他信息
3. 深入理解进程的本质
如下图,进程的前身就是一个可执行程序,它主要包含代码和数据,且在硬盘当中。可执行程序在windows系统中一般以.exe结尾,在Linux系统中一般没有特定的后缀结尾,且通常不跟后缀。
当一个可执行程序启动运行时,其代码与数据将被载入内存之中。在这一过程之前,操作系统内核会创建一个名为task_struct的结构体对象,该对象详细记录了进程的各种属性。这些属性包括但不限于进程标识符(pid)、状态信息、优先级等。特别地,memptr指针负责记录内存中myexe程序的起始地址,确保进程能够正确地访问和执行程序代码。
进程 = 内核数据结构(task_struct) + 程序的代码和数据
当多个可执行程序同时运行时,task_struct结构体内部采用特定的数据结构,将所有进程串联起来。设想task_struct中包含一个指向下一个结构体的指针,那么将存在一个结构体指针list,它指向链表中的第一个task_struct对象。随着新进程的创建,它们的task_struct对象会被前一个对象的next指针串联起来,从而在操作系统中形成了一个调度队列。
CPU的寄存器并不直接存储程序的代码和数据,而是通过list指针定位到相应的task_struct对象,并将其信息载入寄存器。因此,当CPU调度内存中的myexe进程时,实际上是在操作该进程的task_struct对象,进而间接执行程序代码。
我们可以用一个类比来理解这个过程:假设张三正在求职,他提交了一份简历给公司的人力资源部门(HR)。这份简历包含了张三的个人详细信息、实习经历和项目经验,这些信息集合就如同进程的task_struct结构体,记录了张三的“属性”。当HR收到众多简历并逐一审核时,求职者本人对于这一过程是一无所知的。这与CPU调度进程的情形相似,CPU并不直接运行内存中的程序,而是通过操作task_struct结构体对象来间接实现对进程的调度。
4. 进程PID
4.1 指令获取PID
不管是在windows系统中,你双击某个应用,还是在Linux系统中,你执行某些指令或者运行某个可执行程序,当这些程序跑起来都叫做进程。不过进程一般分为两种,第一种是执行完就退出的进程,像Linux系统下的ls pwd等指令;第二种是执行完一直不退出,直到用户关闭,像window系统中的QQ,微信等应用,这种进程叫做常驻型进程。
下面写一份C语言代码,在main函数使用while循环搞个死循环,内部是每隔一秒钟打印一句话。sleep函数的作用是使当前进程暂停执行指定的秒数,该函数的原型通常在<unistd.h>头文件中定义
当程序运行起来,会不断打印“hello linux!”。
这时我们再打开一个Xshell程序,就可以在不打扰上面进程的情况下输入新指令并运行。
我们可以使用ps指令来查看进程信息。其中head -1表示拿到进程信息的第一行,grep加某些进程名,表示拿到该进程名那行信息。你会发现COMMAND这列中含有grep指令,这是因为gerp myproc本身就含有myproc,也会打印出来。想要去掉它,grep后加上-v grep,就是忽略grep相关的文本信息。
-a
:显示所有终端下的进程。-x
:显示没有控制终端的进程(在后台运行)。-j
:显示与作业控制相关的信息。
我们观察上图,第一行中有许多属性名词,其中PID表示进程的唯一标识符,PPID表示父进程的标识符。myproc进程pid是4340。如果我们按下Ctrl+C按键,会中断该进程。
当再次启动进程时,使用ps指令获取进程信息,pid值为6336。你会发现同样都是myproc的进程,pid值发生改变。因为系统使用一个累加的计数器维护进程的PID值,在你启动进程时,操作系统也不断在请求任务。所以,PID值出现变化且不连续是很正常的。
4.2 geipid函数获取PID
getpid是一个系统调用级函数,可以获取进程的id值。需要包含两个头文件,分别是unistd.h和sys/types.h。其中pid_t其实本质就是long int类型,只不过被typedef封装了一下。
使用getpid函数不用频繁获取,只需获取一次,因为一个进程启动之后,id值不会改变的,除非重新启动一次。当myproc程序执行起来后,可以看到进程id值是8042。我们使用ps指令获取myproc进程的信息,会发现pid值也是8042。
4.3 kill指令终止进程
如果你想要终止一个进程,可以在键盘上按下Ctrl + C的按键,发出终止信号。或者使用kill指令,使用kill -l可以查看kill指令的选项,其中9对应的选项就是终止进程。
终止进程操作如上图所示,输入kill + -9 + 进程id值,在另外一恶搞Xshell程序中可以看到myproc进程停止,显示了Killed信息。
4.4 进程信息文件夹
(1)exe
我们刚刚通过ps指令可以获取进程的一部分信息。在Linux操作系统下,一切皆文件。我们使用ls指令查看根目录,会发现有个proc目录。
proc就是process(进程)的缩写,再使用ls指令查看该目录,可以发现许多以数字命名的目录,这些就是某个进程的id值,里面存放的就是该进程的相关信息。
运行myproc程序后,生成一个pid值为19775的进程,使用ls命令查看/proc路径下的文件,其中就有名为19775文件夹。该文件夹存放的就是该进程的详细信息。
我们查看/proc/19775路径下的文件,会发现里面有许多内容。今天需要认识一下exe和cwd。进程是被某个可执行程序启动,exe存放的就是该可执行程序的路径
当我们再打开一个Xshell程序,进入到myproc的目录下,删除该可执行程序,你会发现进程还是在运行。因为进程已经被加载到内存当中,删除硬盘的可执行程序不会造成影响。但是再次查看/proc/19775路径下的文件,会发现exe显示该路径下的可执行程序已被删除。
(2)cwd
cwd全称是current working directory,意思是当前工作目录。我们在C语言中创建一个文件,如果没有指定绝对路径,默认生成在当前路径下。下面我们写个代码验证一下。
运行myproc程序,过几秒终止进程。查看当前目录,会发现多了一个file.txt文件。所以使用fopen新建一个文件时,不是你以为的相对路径,而是拿到cwd再加上你输入的文件名,形成一个绝对路径。还有什么方式可以证明上面的说法呢?
我们可以使用一个系统调用函数chdir,它的作用就是改变当前进程的工作目录。我们在myproc.c中使用chdir函数,修改当前工作目录为home目录下的普通用户,也就是我正在使用的用户。如果修改到根目录或者家目录,无法新建文件夹,因为普通用户没有写权限,只有超级用户才可以新建文件或者目录。
当我们启动myproc程序后,立即查看/proc/24404中内容,会发现cwd变成了普通用户的目录。再使用ls命令查看工作目录,发现有file.txt文件。这就验证了在代码中新建文件,如果不写绝对路径,会在输入的文件名前加上当前工作目录。
5. 进程PPID
5.1 getppid函数获取PPID
ppid是指parent process id,即某个进程的父进程id值。
其中getppid函数,是获取某个进程的父进程id值。
修改一下myproc.c中的代码,获取一下父进程id,并打印出来。
当我们启动myproc程序,运行一会后,终止该程序,再启动该程序,重复三次。你会发现它们的父进程id值都是19496。
我们使用ps指令查看该父进程信息,发现该进程的command(执行的指令)是-bash。其中bash是Linux系统下的命令行解释器,类似于windows系统中的cmd。
因此有个结论,在Linux操作系统启动之后,命令行上执行指令或者执行程序,本质上都是bash的进程创建的子进程,由这些子进程执行我们的代码
5.2 fork函数创建子进程
fork函数也是系统调用函数。函数原型如上,返回值类型是pid_t,不用传参。它的作用是在当前进程下创建一个子进程。
修改myproc.c中的代码,一开始打印一下当前进程的pid和ppid。之后调用fork函数,试着打印子进程的pid和ppid。
运行myproc程序,你会发现打印了三行语句。其中第一行语句是第一个printf函数打印的,pid值为29447的进程应该是myproc启动后的进程它的父进程就是上面提到的bash。
但是下面打印了两行语句,第二行语句的pid值为29447,ppid值是26892,应该是当前程序的进程。最后一行语句中,pid值是29448,ppid值是29447,它的父进程就是myproc程序启动后的进程,说明这是fork函数创建出来的子进程,并且pid值是连续的。
可以得出结论,调用fork函数,当前进程会创建一个子进程,这两个进程会同时执行后面的代码,相当于两个执行流分支。因为这两个进程是连续创建的,它们的pid值连续。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if (id > 0)
{
while (1)
{
printf("我是父进程,pid: %d, ppid: %d, ret id: %d\n", getpid(), getppid(), id);
sleep(1);
}
}
else if (id == 0)
{
while (1)
{
printf("我是子进程,pid: %d, ppid: %d, ret id: %d\n", getpid(), getppid(), id);
sleep(1);
}
}
}
fork函数调用成功,给父进程返回子进程的pid,给子进程返回0。如果调用失败,会设置错误值到errno变量中。
运行结果如上,fork创建进程后,当前进程接收到子进程的id值,子进程接收到0。那么为什么if else语句看起来能同时成立,且有两个返回值?
上面有提到,进程 = 内核数据结构(task_struct) + 程序的代码和数据。那么fork函数创建一个子进程时,操作系统会创建一个task_struct结构体对象。程序中的代码因为是只读的,子进程与父进程共享代码。
而子进程会私有一份数据,因为两个进程间数据互相修改,可能会触发某些条件导致程序崩溃,所以进程之间具有很强的独立性,一个进程崩溃不会影响另外一个进程。这就类似于手机上启动许多应用,如微信,抖音等,如果微信崩溃,不会影响抖音的运行。
因为fork函数创建子进程后,会出现两个执行流,那么此时fork函数返回一个值时,有两个执行流进行返回,并且进程间的数据是各自私有的,那么id变量会有两份。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int g_val = 0;
int main()
{
printf("I am a process, pid: %d, ppid: %d\n", getpid(), getppid());
pid_t id = fork();
if(id > 0)
{
while(1)
{
printf("我是父进程,pid: %d, ppid: %d, ret id: %d, g_val: %d\n", getpid(), getppid(), id, g_val);
sleep(1);
}
}
else if(id == 0)
{
while(1)
{
printf("我是子进程,pid: %d, ppid: %d, ret id: %d, g_val: %d\n", getpid(), getppid(), id, g_val);
g_val++;
sleep(1);
}
}
}
如上我们可以定义一个全局变量g_val,在父进程中只打印出来,在子进程中不仅打印出来,每次让g_val变量加1。
结果如上,父进程的g_val值一直是0,子进程的g_val值不断变化。这就证明了子进程会私有一份数据。
6. 创建多进程
#include <iostream>
#include <unistd.h>
#include <vector>
#include <sys/types.h>
using namespace std;
const int num = 10;
void SubProcessRun()
{
while(true)
{
cout << "I am a sub process, pid: "<< getpid() << " ,ppid: "<<getppid()<<endl;
sleep(5);
}
}
int main()
{
vector<pid_t> childproc;
for(int i = 0; i < num; i++)
{
pid_t id = fork();
if (id == 0)
{
//子进程
SubProcessRun();
}
//走到这里,只能是父进程执行
childproc.push_back(id);
}
//父进程遍历所有子进程的pid
cout << "我的所有子进程:";
for(auto child: childproc)
{
cout<<child << " ";
}
cout << endl;
sleep(10);
while(true)
{
cout << "我是父进程,pid: "<< getpid()<<endl;
sleep(1);
}
return 0;
}
使用一个for循环,使用fork创建子进程。每个子进程再调用SubProcessRun函数,死循环不断打印pid值。 使用vector数组存储子进程的pid值,然后遍历打印出来。父进程也执行个死循环,不退出。
运行该程序,就可以一次性创建多个连续进程。
7. fork函数如何返回两个值
fork函数为什么会返回两个返回值?
因为fork函数内部再返回值之前,会先创建子进程,创建进程会先在内核中创建task_struct结构体对象,用于管理进程的属性,然后代码共享,数据拷贝父进程的。这时,子进程已经开始运行,那么就会有两个执行流,最后返回值也是代码,就会被父子进程各自返回一次。
不过还有一个问题没有弄清楚,return语句返回的是一个值。为什么两个执行流返回时,这个值就变成两个不同的值?这是后面会解决的问题。
创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!