1.实验目的与要求
- 知道进程相关概念
- 获取进程信息
- 会进程的创建和终止
- 会进程的调用
2.实验平台
实验室安装的实验环境(Linux操作系统)和头歌(www.educoder.net)实验平台(课程实验)
3.实验内容
- 获取进程
- 进程的创建和终止
- 进程的调用
4.实验详细内容、步骤
任务描述
在 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 函数名
获取进程自身pid
获取进程本身的进程ID
的系统调用函数是getpid
,具体的说明如下:
- 需要的头文件如下:
- i. #include <sys/types.h>
- ii. #include <unistd.h>
- 函数格式如下:
pid_t getpid(void);
- 函数返回值说明: 返回当前进程的
pid
值。
案例演示1
: 编写一个程序,打印自身的进程ID
。详细代码如下所示:
- #include <stdio.h>
- #include <sys/types.h>
- #include <unistd.h>
- int main()
- {
- pid_t pid = getpid();
- printf("当前进程的ID为:%d\n", pid);
- return 0;
- }
将以上代码保存为getpid.c
文件,编译执行。可以看到每次运行都打印出不同的进程ID
,这是因为Linux
系统动态的给进程分配pid
。
获取父进程pid
获取父进程的进程ID
的系统调用函数是getppid
,具体的说明如下:
- 需要的头文件如下:
- i. #include <sys/types.h>
- ii. #include <unistd.h>
- 函数格式如下:
pid_t getppid(void);
- 函数返回值说明: 返回当前进程的父进程的
pid
值。
案例演示1
: 编写一个程序,打印父进程ID
和自身进程ID
。详细代码如下所示:
- #include <stdio.h>
- #include <sys/types.h>
- #include <unistd.h>
- int main()
- {
- pid_t pid = getpid();
- printf("当前进程的ID为:%d\n", pid);
- pid_t ppid = getppid();
- printf("当前进程的父进程ID为:%d\n", ppid);
- return 0;
- }
将以上代码保存为getppid.c
文件,编译执行。可以看到每次运行都打印出相同的父进程ID
,这是因为我们在同一个终端中运行3
次程序,所以被运行的程序父进程为终端进程,因为父进程一直都一样。
编程要求
本关的编程任务是补全右侧代码片段中Begin
至End
中间的代码,具体要求如下:
- 补全
getProcInfo
函数,用于获取当前进程ID
和其父进程ID
(提示:将结果存放在procIDInfo结构体中)。
测试说明
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
任务描述
在上一关我们学习如何获取进程的pid
信息,本关我们将介绍如何编程创建一个新的进程。
本关任务:学会使用C
语言在Linux
系统中使用fork
系统调用创建一个新的进程。
相关知识
在Linux
系统中创建进程有很多函数可以使用,其中包括了系统调用也包括库函数。本关将介绍一个最常见的系统调用函数来创建进程,这就是使用fork
函数来创建一个新进程。
当用户调用fork
函数时,系统将会创建一个与当前进程相同的新进程。通常将原始进程称为父进程,而把新生成的进程称为子进程。子进程是父进程的一个拷贝,子进程获得同父进程相同的数据,但是同父进程使用不同的数据段和堆栈段。
在早期的系统中,创建进程比较简单。当调用fork
时,内核会把所有的内部数据结构复制一份,复制进程的页表项,然后把父进程的地址空间中的内容也复制到子进程的地址空间中。但是从内核角度来说,这种复制方式是非常耗时的。
因此,在现代的系统中采取了更多的优化。现代的Linux
系统采用了写时复制技术(Copy on Write
),而不是一创建子进程就将所有的数据都复制一份。
Copy on Write
(COW
)的主要思路是:如果子进程/父进程只是读取数据,而不是对数据进行修改,那么复制所有的数据是不必要的。因此,子进程/父进程只要保存一个指向该数据的指针就可以了。当子进程/父进程要去修改数据时,那么再复制该部分数据即可。这样也不会影响到子父进程的执行。因此,在执行fork
时,子进程首先只复制一个页表项,当子进程/父进程有写操作时,才会对所有的数据块进行复制操作。
[COW
思路]
在Linux
系统中可以使用man
命令来查询该函数的使用方法。具体的查询命令为: man 2 函数名
使用fork函数创建进程
fork
函数的具体的说明如下:
- 需要的头文件如下:
- i. #include <unistd.h>
- 函数格式如下:
pid_t fork(void);
- 函数返回值说明: 调用成功,
fork
函数两个值,分别是0
和子进程ID
号。当调用失败时,返回-1
,并设置错误编号errno
。
注意:fork
函数调用将执行两次返回,它将从父进程和子进程中分别返回。从父进程返回时的返回值为子进程的 PID
,,而从子进程返回时的返回值为0
,并且返回都将执行fork
之后的语句。
案例演示1: 编写一个程序,使用fork
函数创建一个新进程,并在子进程中打印出其进程ID
和父进程ID
,在父进程中返回进程ID
。详细代码如下所示:
- #include <stdio.h>
- #include <sys/types.h>
- #include <unistd.h>
- #include <string.h>
- #include <errno.h>
- 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
来退出程序)。
测试说明
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
任务描述
在上一关我们学习使用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
函数的具体的说明如下:
- 需要的头文件如下:
- i. #include <sys/types.h>
- ii. #include <unistd.h>
- 函数格式如下:
pid_t vfork(void);
- 函数返回值说明: 调用成功,
vfork
函数两个值,分别是0
和子进程ID
号。当调用失败时,返回-1
,并设置错误编号errno
。
注意:vfork
函数调用将执行两次返回,它将从父进程和子进程中分别返回。从父进程返回时的返回值为子进程的 PID
,,而从子进程返回时的返回值为0
,并且返回都将执行vfork
之后的语句。vfork
创建的子进程必须调用exit
函数来退出子进程。
案例演示1
: 编写一个程序,使用vfork
函数创建一个新进程,并在子进程中打印出其进程ID
和父进程ID
,在父进程中返回进程ID
。详细代码如下所示:
- #include <stdio.h>
- #include <sys/types.h>
- #include <unistd.h>
- #include <stdlib.h>
- #include <string.h>
- #include <errno.h>
- 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"字符串(提示:需要换行)。
测试说明
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
任务描述
在上一关我们学习使用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
函数的具体的说明如下:
- 需要的头文件如下:
- i. #include <stdlib.h>
- 函数族格式如下:
- i. void exit(int status);
参数说明: status:设置程序退出码;
_exit
函数的具体的说明如下:
- 需要的头文件如下:
- i. #include <unistd.h>
- 函数族格式如下:
- i. void _exit(int status);
参数说明: status
:设置程序退出码;
- 函数返回值说明:
exit
和_exit
均无返回值。
atexit
和on_exit
使用方法
atexit
和on_exit
函数的具体的说明如下:
- 需要的头文件如下:
- i. #include <stdlib.h>
- 函数族格式如下:
- i. int atexit(void (*function)(void));
- ii. 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 <stdlib.h>
- #include <stdio.h>
- 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 <stdlib.h>
- #include <stdio.h>
- 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
和assert
使用方法
abort
函数的具体的说明如下:
- 需要的头文件如下:
- i. #include <stdlib.h>
- 函数族格式如下:
- i. void abort(void);
assert
宏的具体的说明如下:
- 需要的头文件如下:
- i. #include <assert.h>
- 函数族格式如下:
- i. void assert(scalar expression);
参数说明: expression
:需要被判断的表达式;
注意:assert
宏通常用于调试程序。
- 函数返回值说明:
abort
和assert
无返回值。
案例演示1: 使用abort
终止一个程序,详细代码如下所示:
- #include <stdlib.h>
- #include <stdio.h>
- int main()
- {
- printf("Hello world\n");
- abort();
- }
将以上代码保存为abort.c
文件,编译执行。可以看到执行abort
程序后,程序被强行终止。
编程要求
本关的编程任务是补全右侧代码片段中Begin
至End
中间的代码,具体要求如下:
- 补全
exitProcess
函数,使用atexit
函数注册一个函数,在注册函数中打印出当前进程的ID
号。
测试说明
本关的测试需要用户在右侧代码页中补全代码,然后点击评测按钮,平台会自动验证用户是否按照要求去检测结果。
任务描述
fork()
函数通过系统调用创建一个与原来进程几乎完全相同的进程。那么,进程中的变量父进程和子进程是否都能使用并修改呢?
围绕问题的提出,我们尝试在父子进程中都修改同一个文件中的内容,最终将文件内容输出,便可知晓答案。
下面我们通过学习相关知识并编写代码来测试你的猜想是否正确。
相关知识
Linux 进程中的几个状态:
- R 运行状态 (
runing
),并不意味着进程一定在运行中,也可以在运行队列里 - S 睡眠状态 (
sleeping
),进程在等待事件完成(浅度睡眠,可以被唤醒) - D 磁盘睡眠状态 (
Disk sleep
),不可中断睡眠(深度睡眠,不可以被唤醒,通常在磁盘写入时发生) - T 停止状态 (
stopped
),可以通过发送 SIGSTOP 信号给进程来停止进程,可以发送 SIGCONT 信号让进程继续运行 - Z 僵尸状态 (
zombie
),子进程退出,父进程还在运行,但是父进程没有读到子进程的退出状态,子进程进入僵尸状态
为了完成本关任务,你需要掌握:1. 如何创建进程;2.fork()
函数的执行步骤。
进程的创建
在使用创建进程函数前,我们需要先导入unistd.h
库。
创建进程的函数原型是:pid_t fork(void);
例如:
- pid_t pid = fork();
pid_t
是一个整数类型,即fork()
函数会返回新进程的 ID 号(0~32768
的整数)。fork
函数在父进程中返回子进程的pid
,在子进程中返回0
。
注意在子进程中返回的0
,并不是子进程的pid
,子进程的pid
在父进程的返回值中保存。而子进程的返回值是为了标识它是子进程,用来区分父子进程的。
父子进程的注意事项:
- 新进程是当前进程的子进程。
- 父进程和子进程 ①父进程:
fork()
的调用者; ②子进程:新建的进程。 - 子进程是父进程的复制(相同的代码,相同的数据,相同的堆栈),除了 ID 号和时间信息外,两者完全相同。
- 子进程和父进程可以并发运行。
fork()
函数的执行步骤
由于子进程是父进程的复制,所以子进程中也会有创建子进程的语句,如果不加以限制,就会形成递归创建,但实际上并不是这样的。
实际流程是:父进程创建了子进程后,子进程中“创建进程”语句不再执行,并发运行其他语句。
在 Linux 的源码中我们可以找到fork
函数:
- ...
- copy_files(clone_flags,p); //克隆文件
- copy_fs(clone_flags,p); //克隆文件系统
- copy_mm(clone_flags,p); //克隆内存信息
- ...
我们可以看到有三条语句,用于拷贝进程的所有信息,这也解释了为什么说子进程是父进程的复制。
编程要求
通过提示,在右侧编辑器中补充代码,完成在指定文件中添加内容,具体要求如下:
- 创建进程;
- 父进程向文件中添加
hello
和world
!
; - 子进程向文件中添加
hello
和welcome
!
; - 只需修改文件内容即可,平台将为你输出文件内容。
提示:fork()
函数的返回值为0
时则为子进程。
测试说明
平台会对你编写的代码进行测试:
预期输出: hello world!
hello welcome!
任务描述
Linux 中,init
进程(初始化进程)是所有其他进程的父进程,那么是不是说就所有的进程都执行与init
进程相同的功能了呢?
答案当然不是,Linux 中某些子进程和父进程的执行并不是完全相同的。他们是如何做到的呢?
下面我们就一起来学习进程的加载。
相关知识
为了完成本关任务,你需要掌握如何加载非父进程的进程。
exec
函数族
Linux 中exec
函数族,它是若干函数的集合。exec
函数族的作用是根据指定的文件名或目录名找到可执行文件,并用它来取代调用进程的内容。换句话说,其功能是让子进程具有和父进程完全不同的新功能。
exec
本身并非一个函数,是指一组函数,一共有6
种在进程中启动另一个程序的方法:
int execl(const char *path, const char *arg, ...);
int execv(const char *path, char *const argv[]);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execve(const char *path, char *const argv[], char *const envp[]);
int execlp(const char *file, const char *arg, ...);
int execvp(const char *file, char *const argv[]);
exec
函数族的6
个函数看起来十分复杂,实际上无论是作用还是用法都十分相似,他们的命名规则:
l
和v
表示参数是以列表还是以数组的方式提供,且都必须以NULL
结尾;- **
p
**代表在path
环境变量中搜索file
文件; e
表示该函数取envp[]
数组,而不使用当前环境,即为程序提供新环境变量,一般很少使用。
进程调用一种 exec
函数时,该进程完全由新程序替换,而新程序则从其 main
函数开始执行。exec
只是用磁盘上的一个新程序替换了当前进程的正文段,数据段,堆段和栈段。并没有创建新进程,所以进程的 ID 是不变的。
注意: 一旦exec
函数执行成功,它就不会返回了,进程结束。但是如果exec
函数执行失败, 它会返回失败的信息,并且进程继续执行后面的代码!执行失败的话,必须用exit()
函数来让子进程退出!(exit
函数调用需导入stdlib.h
库函数)
进程加载
我们使用execl()
函数来做示例:
- int execl(const char *path, const char *arg, ...)
函数参数说明: path
:要执行的程序路径。可以是绝对路径或者是相对路径。在execv
、execve
、execl
和execle
这4
个函数中,使用带路径名的文件名作为参数。 arg
:程序的第0
个参数,即程序名自身。相当于argv[O]
。 …
:命令行参数列表。调用相应程序时有多少命令行参数,就需要有多少个输入参数项。最后应该以NULL
结尾,表明命令行参数结束。
返回值:-1
表明调用exec
失败,无返回表明调用成功。
- #include <unistd.h>
- int main()
- {
- printf("before exec\n");
- execl("/bin/ls", "ls", "-a", "-l", "-h", NULL);
- //若 execl() 执行成功,下面则不执行,因为当前进程已经被执行的 ls 替换了
- printf("after exec\n");
- return 0;
- }
执行语句说明: /bin/ls
:外部程序,这里是/bin
目录的ls
可执行程序,必须带上路径(相对或绝对) ls
:没有意义,如果需要给这个外部程序传参,这里必须要写上字符串,至于字符串内容任意 -a
,-l,-h
:给外部程序ls
传的参数 NULL
:代表给外部程序 ls
传参结束
执行结果:
编程要求
在右侧编辑器补充代码,要求如下:
- 创建进程;
- 在父进程中输出
entering main process---
; - 在子进程中使用
execl()
函数调用src/step2/hello.c
文件,测试环境中已将path
置为src/step2
,因此带路径的文件名为./hello
测试说明
平台会对你编写的代码进行测试:
预期输出: entering main process---
Hello exec! This is another task。
任务描述
学习完进程的创建和加载,我们发现系统都是先执行父进程的内容再执行子进程,那么有什么方法可以使子进程先执行,父进程后执行的吗?
通过学习相关知识,我们需要编写一个先执行子进程内容后执行父进程内容的程序。
相关知识
为了完成本关任务,你需要掌握:1. 系统进程退出方法;2.系统进程等待方法。
进程退出
进程常见退出方式:
1.正常退出:从main()
函数中返回return
退出;调用exit()
函数退出;调用_exit()
函数退出。
2.异常退出:由信号终止;调用abort
函数。
- return:是常见的退出进程方式,
main
函数中执行return
等同于执行exit
函数,main
函数中return n
;
函数返回值作为exit(n)
函数的参数。 - exit():进程结束执行时调用,完成进程资源回收。函数原型:
void exit(int status);
包含在stdlib.h
库中,参数status
为进程的终止状态,父进程可以通过wait()
获取。exit(0)
表示正常退出,exit(n)
其中n
不为0
都表示异常退出。 - _exit():正常退出的方式最终都会调用
_exit()
函数。函数原形:void _eixt(int status);
包含在unistd.h
库中,参数同exit
函数。
进程等待
创建子进程后如果父进程不等待,子进程退出后就会变成僵尸进程,直到父进程来获取退出信息才会释放剩余资源,并且此时该进程无法被信号杀死,继续占用资源造成内存泄露。因此,我们需要父进程调用等待函数来避免出现僵尸进程,进程等待函数是为了配合子进程的exit()
,进而实现释放子进程资源。
进程等待方式,使用前需导入sys/wait.h
库:
- wait() 是一个阻塞式等待,必须等到有一个子进程退出后获取退出状态,释放资源才可以返回。
- pid_t wait(int * status);
返回值: 退出的子进程的pid
,失败返回-1
。
参数: 输出型参数,用于获取子进程退出状态码,不关心可以置为NULL
。
- waitpid() 是一个指定
pid
的等待方式。
- pid_t waitpid(pid_t pid, int * status, int options);
返回值: 返回退出进程的pid
,当调用失败(没有子进程)返回-1
。可以通过perror
函数进行打印错误。
参数: pid
:pid=-1
表示等待任一进程;pid=n
则n
为指定需要等待的子进程,若n<0
则等待和其绝对值的相同的子进程;pid=0
表示等待和进程组pid
相同的进程。
status
:同wait
函数参数status
相同。
options
:选项参数,options=0
表示和wait()
一样的阻塞等待;options=WNOHANG
表示不阻塞,如果没有退出的进程或者需要等待的子进程将直接返回0
,另外这个参数还可以设置成其它属性。
编程要求
根据提示,在右侧编辑器补充代码,创建两个子进程,第一个进程打印I am first process!
,第二个进程打印I am second process!
,父进程打印I am father process!
。
要求实现先打印第一个进程内容,再打印第二个进程内容,最后打印父进程内容。
提示:进程加载execl
函数调用输出用法execl("/bin/echo","echo", "输出语句", NULL);
测试说明
平台会对你编写的代码进行测试:
预期输出: I am first process!
I am second process!
I am father process!