进程的概念是操作系统的核心。因此理解进程、学会进程的操作非常重要。为了充分呈现Linux下进程的相关知识,将以多篇博客的形式展现。本篇重点介绍进程的相关概念以及如何利用fork()创建子进程。
目录
进程的概念
什么是进程,书本上进程有如下的几个定义:
一个正在执行的程序。
一个正在计算机上执行的程序实例。
能分配给处理器并由处理器执行的实体。
由一组执行的指令、一个当前状态和一组相关的系统资源表征的活动单元
也可以把进程视为由一组元素组成的实体,进程的两个基本元素是程序代码和与代码相关联的数据集。假设处理器开始执行这个程序代码,我们把这个执行实体称为进程。
总的来说,进程等价于程序文件内容+相关数据结构
进程控制块-PCB
我们知道,曾经我们所有启动程序的过程,本质上都是在系统上创建进程。系统中当然可能存在着大量进程,操作系统需要对这些进程进行管理,所谓的管理手段即先描述,再组织。
描述进程所依靠的正是进程控制块(PCB),PCB究竟是什么呢?在学习C语言的过程中,如果需要让你描述一名学生的信息,你一定会立刻想到结构体。没错,PCB的本质就是C语言上的结构体,现在PCB在我们的眼中就是下面的形式:
struct PCB
{
//进程的所有属性
};
在任何进程形成之前,操作系统就要为该进程创建PCB。而在Linux操作系统上,PCB就具体化为task_struct。
有了进程控制块,所有的进程管理任务与进程对应的程序毫无关系,与进程对应的内核创建的该数据结构强相关。
task_struct内容分类
task_struct存储进程的相关属性,具体内容如下:
标识符 | 描述本进程的唯一标识符,用来区别其他进程 |
状态 | 任务状态、退出代码、退出信号等 |
优先级 | 相对于其他进程的优先级 |
程序计数器 | 程序中即将被执行的下一条指令的地址 |
内存指针 | 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块指针 |
上下文数据 | 进程执行时处理器的寄存器中的数据 |
I/O状态信息 | 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表 |
记账信息 | 可能包括处理器时间总和,使用的时钟数总和,时间限制,记帐号等 |
其他信息 |
下面将针对一些属性做更加具体的说明。
(1)PID
PID是Linux中描述进程的唯一表示符。先写下如下测试程序:
#include <iostream>
#include <unistd.h>
int main()
{
while(true)
{
sleep(2);
std::cout << "I am a proc" << std::endl;
}
return 0;
}
运行进程后每隔两秒打印一句话
此时使用指令ps axj | head -n1 && ps axj | grep myproc
可以得知13317就是刚才创建进程的PID。
同时,PPID指的是父进程的PID,我们可以使用ps axj | head -n1 && ps axj | grep 10062查看进程的父进程。
可以看到,父进程就是我们所熟知的bash。
在程序中,我们也可以使用getpid()和getppid()来获取本进程以及父进程的pid。
测试代码如下:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
while(true)
{
sleep(2);
cout << "pid:" << getpid() << " ppid:" << getppid() << endl;
}
return 0;
}
运行结果:
(2)退出码
在学习C语言的过程中,在main函数中,我们一般都会在最后加上return 0,这里所return的0就是退出码,它将被返回给操作系统。一般而言,0表示进程正常结束,否则表示异常。
在Linux下,我们可以使用指令echo $?查看最近退出进程的退出码。测试代码如下:
#include <iostream>
#include <sys/types.h>
#include <unistd.h>
int main()
{
return 1;
}
结果如下:
如果我们继续使用echo $?指令,新的退出码就会变为0
这是因为此时最近结束的指令是上一句echo $?,此时显示的是它的退出码。
(3)上下文数据
在实际进程中,代码可能不是很短时间就能结束的,如果有多个进程同时存在,且进程数大于CPU数,CPU是绝对不会将一个进程彻底执行完再去执行新的进程的,因为会导致很多进程根本就没有执行的机会。因此规定了每个进程单次运行的时间片,如10ms,时间片一旦到了,就会将该进程切换走,运行其它进程。也就是说进程在运行期间是有切换的!那么这和上下文数据有什么关系呢?
进程在被切换之前,可能存在大量的临时数据,这些数据被暂时存放在CPU的寄存器中。但是CPU中寄存器只有一套,如果进程在被切换之前什么都不做,下一次轮到这个进程运行的时候,寄存器中的内容已经被其它进程产生的临时数据所覆盖,这个进程又如何接着之前继续运行呢?
因此,一个进程在被切换之前,会将寄存器中的数据暂存在PCB中(保护上下文),回来时再将上下文数据返回到寄存器(恢复上下文)。
查看进程信息
查看进程信息可以通过在/proc系统文件夹查看。
利用系统调用fork创建子进程
fork的功能是创建子进程,使用方式也简洁明了。写下如下测试代码:
#include <iostream>
#include <unistd.h>
int main()
{
fork();
std::cout << "pid: " << getpid() << " ppid: " << getppid() << std::endl;
return 0;
}
可以看到运行后输出了两边pid和ppid的显示,即一个为子进程输出,一个为父进程输出。同时我们也发现了子进程和父进程做了相同的事情。
如何理解fork创建子进程?fork的本质是创建进程,即系统会多了一个进程,与进程相关的内核数据结构+进程的代码和数据在系统中也多了一份吗?默认情况下,子进程会继承父进程的代码和数据,内核数据结构也会以父进程为模板,初始化子进程的task_strcut。
也正是因为子进程的task_strcut是以父进程的task_strcut为模板的,因此它们的程序计数器是相同的,两个进程将沿着创建子进程后的代码执行。
fork()
{
……//创建子进程的过程
return XXX;//父子进程从此处开始执行
}
可以简单地认为fork之后,父进程与子进程的代码是共享的,因为代码是不可修改的。默认情况下,数据也是共享的,不过需要考虑修改数据的情况。如果进程要对某一数据进行修改,需要通过写时拷贝来保证进程数据的独立性。
写下如下测试代码:
#include <iostream>
#include <unistd.h>
int main()
{
pid_t id = 0;
id = fork();
std::cout << "id = " << id << std::endl;
return 0;
}
运行结果:
对同一个值我们获得了两个结果,这就是写时拷贝所导致的。同时我们也用到了fork()的返回值,它会为父进程返回子进程的pid,为子进程返回0。如此设计的原因是实际中只可能出现一个父进程有n个子进程的情况,因此父进程如果想找到指定的子进程,必须要获得该子进程的具体pid;而子进程要找到父进程,只需要getppid()即可(因为一个子进程只可能对应一个父进程)。
那么我们创建子进程是希望子进程和父进程做相同的事情吗?大多数情况下,这样是没有意义的。此时我们就可以利用fork()的返回值来使子进程和父进程做相同的事情。
测试代码如下:
#include <iostream>
#include <unistd.h>
int main()
{
pid_t id = 0;
id = fork();
if(id > 0)
{
//parent
std::cout << "parent is run" << std::endl;
}
else
{
//child
std::cout << "child is run" << std::endl;
}
return 0;
}
运行结果:
而fork()之后父子进程谁先运行是不确定的,需要由调度器控制。