Linux:进程入门(进程与程序的区别,进程的标识符,fork函数创建多进程)
往期文章:《Linux:深入了解冯诺依曼结构与操作系统》
目录
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语句返回的是一个值。为什么两个执行流返回时,这个值就变成两个不同的值?这是后面会解决的问题。
创作充满挑战,但若我的文章能为你带来一丝启发或帮助,那便是我最大的荣幸。如果你喜欢这篇文章,请不吝点赞、评论和分享,你的支持是我继续创作的最大动力!
原文地址:https://blog.csdn.net/2301_79171011/article/details/142502225
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!