在 Linux 环境下,进程是一个十分重要的概念。每个进程都由一个唯一的标识符来表示,即进程ID,通常称为pid。本关将介绍如何获取进程的pid。
本关任务:学会使用C语言在Linux系统中获取进程的pid以及父进程的pid。
相关知识Linux系统中存在一个特殊的进程,即空闲进程(idle process),当没有其他进程在运行时,内核所运行的进程就是空闲进程,它的pid为0。在启动后,内核运行的第一个进程称为init进程,它的pid是1。通常,Linux系统中init进程就是我们在资源管理器中看到的名为init的程序。系统中其它的进程都是由init来创建出来的。
创建新进程的那个进程被称为父进程,而新创建的进程被称为子进程。每个进程都是由其他进程创建的(除了init进程),因此每个子进程都有一个父进程。
Linux系统提供了两个系统调用函数来获取一个进程的pid和其父进程的pid,分别是getpid和getppid函数。在Linux系统中可以使用man命令来查询这些函数的使用方法。具体的查询命令为:
man 2 函数名
获取进程本身的进程ID的系统调用函数是getpid,具体的说明如下:
-
需要的头文件如下:
- #include
- #include
- 函数格式如下:
pid_t getpid(void);
-
函数返回值说明:
返回当前进程的pid值。
案例演示1: 编写一个程序,打印父进程ID和自身进程ID。详细代码如下所示:
- #include
- #include
- #include
- int main()
- {
- pid_t pid = getpid();
- printf("当前进程的ID为:%dn", pid);
- pid_t ppid = getppid();
- printf("当前进程的父进程ID为:%dn", ppid);
- return 0;
- }
将以上代码保存为getppid.c文件,编译执行。可以看到每次运行都打印出相同的父进程ID,这是因为我们在同一个终端中运行3次程序,所以被运行的程序父进程为终端进程,因为父进程一直都一样。
编程要求本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下:
- 补全getProcInfo函数,用于获取当前进程ID和其父进程ID(提示:将结果存放在procIDInfo结构体中)。
测试说明
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
#include第2关:进程创建操作-fork 任务描述#include #include struct procIDInfo { pid_t pid; pid_t ppid; }; struct procIDInfo getProcInfo() { struct procIDInfo ret; //存放进程ID信息,并返回 ret.pid=getpid(); ret.ppid=getppid(); return ret; }
在上一关我们学习如何获取进程的pid信息,本关我们将介绍如何编程创建一个新的进程。
本关任务:学会使用C语言在Linux系统中使用fork系统调用创建一个新的进程。
相关知识在Linux系统中创建进程有很多函数可以使用,其中包括了系统调用也包括库函数。本关将介绍一个最常见的系统调用函数来创建进程,这就是使用fork函数来创建一个新进程。
当用户调用fork函数时,系统将会创建一个与当前进程相同的新进程。通常将原始进程称为父进程,而把新生成的进程称为子进程。子进程是父进程的一个拷贝,子进程获得同父进程相同的数据,但是同父进程使用不同的数据段和堆栈段。
在早期的系统中,创建进程比较简单。当调用fork时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容也复制到子进程的地址空间中。但是从内核角度来说,这种复制方式是非常耗时的。
因此,在现代的系统中采取了更多的优化。现代的Linux系统采用了写时复制技术(Copy on Write),而不是一创建子进程就将所有的数据都复制一份。
Copy on Write(COW)的主要思路是:如果子进程/父进程只是读取数据,而不是对数据进行修改,那么复制所有的数据是不必要的。因此,子进程/父进程只要保存一个指向该数据的指针就可以了。当子进程/父进程要去修改数据时,那么再复制该部分数据即可。这样也不会影响到子父进程的执行。因此,在执行fork时,子进程首先只复制一个页表项,当子进程/父进程有写操作时,才会对所有的数据块进行复制操作。
[COW思路]
在Linux系统中可以使用man命令来查询该函数的使用方法。具体的查询命令为:
man 2 函数名
使用fork函数创建进程
fork函数的具体的说明如下:
-
需要的头文件如下:
- #include
- #include
-
函数格式如下:
pid_t fork(void); -
函数返回值说明:
调用成功,fork函数两个值,分别是0和子进程ID号。当调用失败时,返回-1,并设置错误编号errno。
注意:fork函数调用将执行两次返回,它将从父进程和子进程中分别返回。从父进程返回时的返回值为子进程的 PID,,而从子进程返回时的返回值为0,并且返回都将执行fork之后的语句。
案例演示1:
编写一个程序,使用fork函数创建一个新进程,并在子进程中打印出其进程ID和父进程ID,在父进程中返回进程ID。详细代码如下所示:
- #include
- #include
- #include
- #include
- #include
- int main()
- {
- pid_t pid;
- pid = fork();
- if(pid == -1)
- {
- //创建进程失败
- printf("创建进程失败(%s)!n", strerror(errno));
- return -1;
- }
- else if(pid == 0)
- {
- //子进程
- printf("当前进程为子进程:pid(%d),ppid(%d)n", getpid(), getppid());
- }
- else
- {
- //父进程
- printf("当前进程为父进程:pid(%d),ppid(%d)n", getpid(), getppid());
- }
- //子进程和父进程分别会执行的内容
- return 0;
- }
将以上代码保存为forkProcess.c文件,编译执行。可以看到每次执行forkProcess时,子进程和父进程都不是固定的执行顺序,因此由fork函数创建的子进程执行顺序是由操作系统调度器来选择执行的。因此,子进程和父进行在执行的时候顺序不固定。
编程要求本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下:
- 补全createProcess函数,使用fork函数创建进程,并在子进程中输出"Children"字符串,在父进程中输出"Parent"字符串。(注意:不要在createProcess函数中使用exit函数或者return来退出程序)。
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
#include第3关:进程创建操作-vfork 任务描述#include #include #include #include void createProcess() { pid_t pid=fork(); if(pid==-1) printf("创建进程失败!"); else if(pid==0) printf("Children"); else printf("Parent"); }
在上一关我们学习使用fork函数创建新进程,本关我们将介绍如何另一种创建新进程的系统调用函数。
本关任务:学会使用C语言在Linux系统中使用vfork系统调用创建一个新的进程。
相关知识在上一关卡中,我们介绍了fork的使用方法。使用fork创建的子进程的特点是:(1)子进程采用写时复制(COW)技术来为子进程创建地址空间;(2)子进程和父进程的执行顺序是由操作系统调度器来决定的。
本关将介绍Linux系统中另一个创建进程的系统调用函数vfork。vfork函数是一个历史遗留产物。vfork创建进程与fork创建的进程主要有一下几点区别:
- vfork创建的子进程与父进程共享所有的地址空间,而fork创建的子进程是采用COW技术为子进程创建地址空间;
- vfork会使得父进程被挂起,直到子进程正确退出后父进程才会被继续执行,而fork创建的子进程与父进程的执行顺序是由操作系统调度来决定。
vfork性能要比fork高,主要原因是vfork没有进行所有数据的复制,尽管fork采用了COW技术优化性能,但是也会为子进程的页表项进行复制,因此vfork要比fork快。
使用vfork时要注意,在子进程中对共享变量的修改也会影响到父进程,因此vfork在带来高性能的同时,也使得整个程序容易出错,因此,开发人员在使用vfork创建进程时,一定要注意对共享数据的修改。
由于vfork创建的子进程和父进程共享所有的数据(栈、堆等等),因此,采用vfork创建的子进程必须使用exit或者exec函数族(下一关将介绍这些函数的功能)来正常退出,不能使用return来退出。
exit函数是用来结束正在运行的整个程序,exit是系统调用级别,它表示一个进程的结束;而return 是语言级别的,它表示调用堆栈的返回。
vfork函数是系统调用函数,man 2 vfork来查看其使用方法。而exit函数是库函数,因此使用man 3 exit来查看其使用方法。
使用vfork函数创建进程vfork函数的具体的说明如下:
-
需要的头文件如下:
- #include
- #include
- #include
-
函数格式如下:
pid_t vfork(void); -
函数返回值说明:
调用成功,vfork函数两个值,分别是0和子进程ID号。当调用失败时,返回-1,并设置错误编号errno。
注意:vfork函数调用将执行两次返回,它将从父进程和子进程中分别返回。从父进程返回时的返回值为子进程的 PID,,而从子进程返回时的返回值为0,并且返回都将执行vfork之后的语句。vfork创建的子进程必须调用exit函数来退出子进程。
案例演示1: 编写一个程序,使用vfork函数创建一个新进程,并在子进程中打印出其进程ID和父进程ID,在父进程中返回进程ID。详细代码如下所示:
- #include
- #include
- #include
- #include
- #include
- #include
- int main()
- {
- pid_t pid;
- pid = vfork();
- if(pid == -1)
- {
- //创建进程失败
- printf("创建进程失败(%s)!n", strerror(errno));
- return -1;
- }
- else if(pid == 0)
- {
- //子进程
- sleep(2); //睡眠2秒
- printf("当前进程为子进程:pid(%d),ppid(%d)n", getpid(), getppid());
- }
- else
- {
- //父进程
- printf("当前进程为父进程:pid(%d),ppid(%d)n", getpid(), getppid());
- }
- //子进程和父进程分别会执行的内容
- exit(0);
- }
将以上代码保存为vforkProcess.c文件,编译执行。可以看到vforkProcess创建的子进程尽管使用sleep函数睡眠了2秒,但是函数父进程的执行顺序在子进程后,这就是vfork的特性。
当我们将以上代码中的exit(0)换成return 0时,则会出现如下错误。
出现以上错误的原因是当子进程使用return退出时,操作系统也会把栈清空,那么当父进程继续使用return退出时,则会发现栈已经被清空了,这就相当于free两次同一块内存,因此会出现错误。
本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下:
- 补全createProcess函数,使用vfork函数创建进程,并在子进程中输出"Children"字符串(提示:需要换行),在父进程中输出"Parent"字符串(提示:需要换行)。
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
#include第4关:进程终止 任务描述#include #include #include #include #include void createProcess() { pid_t pid=vfork(); if(pid==-1) printf("创建进程失败!n"); else if(pid==0) printf("Childrenn"); else printf("Parentn"); exit(0); }
在上一关我们学习使用vfork函数创建新进程,并且使用exit来结束子进程,本关我们将介绍Linux系统中结束进程的其它方法。
本关任务:学习终止进程的常见方法。
相关知识在上一关以及看到,开发人员使用vfork创建出来的子进程可以用exit函数来结束。在 Linux 环境中,一个进程的结束,可以通过调用相应的函数实现,也可以是接收到某个信号而结束。
常见与退出进程相关的函数有:exit、_exit、atexit、on_exit、abort和assert。
- exit函数是标准C库中提供的函数,它用来终止正在运行的程序,并且关闭所有I/O标准流。
- _exit函数也可用于结束一个进程,与exit函数不同的是,_exit不会关闭所有I/O标准流。
- atexit 函数用于注册一个不带参数也没有返回值的函数以供程序正常退出时被调用。
- on_exit 函数的作用与atxeit函数十分类似,不同的是它注册的函数具有参数,退出状态和参数arg都是传递给该程序使用的。
- abort 函数其实是用来发送一个SIGABRT信号,这个信号将使当前进程终止。
- assert是一个宏。调用assert时,它将先计算参数表达式 expression的值,如果为0,则调用abort函数结束进程。
[exit和_exit区别]
以上关于退出处理函数中只有_exit是系统调用函数,因此使用man 2 _exit来查看其使用方法,而其余函数都是库函数,因此使用man 3 函数名来查看其使用方法。
exit和_exit使用方法exit函数的具体的说明如下:
- 需要的头文件如下:
- #include
- #include
- 函数族格式如下:
- void exit(int status);
status:设置程序退出码;
_exit函数的具体的说明如下:
-
需要的头文件如下:
- #include
- #include
-
函数族格式如下:
- void _exit(int status);
参数说明:
status:设置程序退出码; -
函数返回值说明:
exit和_exit均无返回值。
atexit和on_exit函数的具体的说明如下:
-
需要的头文件如下:
- #include
- #include
-
函数族格式如下:
- int atexit(void (*function)(void));
- int on_exit(void (*function)(int , void *), void *arg);
参数说明:
atexit函数的function参数是一个函数指针,指向无返回值和无参数的函数;
on_exit函数的function参数是一个函数指针,指向无返回值和有两个参数的函数,其中第一个参数是调用exit()或从main中返回时的值,参数arg指针会传给参数function函数; -
函数返回值说明:
atexit和on_exit调用成功返回0;调用失败返回一个非零值。
注意:atexit和on_exit只有在程序使用exit或者main中正常退出时才会有效。如果程序使用_exit、abort或assert退出程序时,则不会执行被注册的函数。
案例演示1: 使用atexit注册一个退出函数,使其在调用退出函数前被执行,详细代码如下所示:
- #include
- #include
- void out()
- {
- printf("程序正在被退出n");
- }
- int main()
- {
- if(atexit(out) != 0)
- {
- printf("调用atexit函数错误n");
- }
- return 0; //或者exit(0)
- }
将以上代码保存为atexit.c文件,编译执行。可以看到执行atexit程序后,out函数被调用。
案例演示2: 使用on_exit注册一个退出函数,使其在调用退出函数前被执行,详细代码如下所示:
- #include
- #include
- void out(int status, void *arg)
- {
- printf("%s(%d)n", (char *s)arg, status);
- }
- int main()
- {
- if(on_exit(out, "程序正在被退出") != 0)
- {
- printf("调用on_exit函数错误n");
- }
- exit(1); //或者return 1
- }
将以上代码保存为on_exit.c文件,编译执行。可以看到执行on_exit程序后,out函数被调用,并且status变量的值就是exit函数退出的值。
abort函数的具体的说明如下:
- 需要的头文件如下:
- #include
- #include
- 函数族格式如下:
- void abort(void);
assert宏的具体的说明如下:
- 需要的头文件如下:
- #include
- 函数族格式如下:
- void assert(scalar expression);
expression:需要被判断的表达式;
注意:assert宏通常用于调试程序。
- 函数返回值说明:
abort和assert无返回值。
案例演示1:
使用abort终止一个程序,详细代码如下所示:
- #include
- #include
- int main()
- {
- printf("Hello worldn");
- abort();
- }
将以上代码保存为abort.c文件,编译执行。可以看到执行abort程序后,程序被强行终止。
本关的编程任务是补全右侧代码片段中Begin至End中间的代码,具体要求如下:
- 补全exitProcess函数,使用atexit函数注册一个函数,在注册函数中打印出当前进程的ID号。
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
#include#include #include #include void exitProcess() { void xiu() { printf("%d",getpid()); } if(atexit(xiu)!=0) { printf("调用atexit函数错误!"); } }



