进程控制 [fork() exec() wait() waitpid()]

x86/Debian Linux/gcc


1 fork()创建子进程

(1) FORK(2)  Linux  Programmer’s  Manual

[头文件及原型]

#include <unistd.h>

pid_t   fork(void)

[功能简述]

fork ----通过复制调用fork的进程创建一个新进程。


[返回值]

创建子进程成功时,fork在父进程中返回子进程的pid,在子进程中返回0。失败时,在父进程中返回-1,无子进程被创建,相应的错误在errno中设置。


(2) fork()创建子进程

[多个进程的运行方式]

每一个进程都有3个状态:阻塞,就绪,运行。当正在运行的进程因为某种原因成为阻塞状态时,已经就绪的多个进程谁被先运行取决于内核基于进程的优先级和时间片的调度算法。


[fork创建子进程代码]

#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
	char	*msg;
	int	n;
	pid_t	pid;

	pid	= fork();
	if (pid < 0) {
		perror("fork failed");
		exit(1);
	}
	if (pid == 0) {
		msg	= "This is in child progress\n";
		n	= 6;
	} else {
		msg	= "This is in parent progress\n";
		n	= 3;
	}

	for (; n > 0; --n) {
		printf("%s", msg);
		//sleep(1);
	}
	return 0;
}

[编译并执行程序]

gcc -o  pro_control  pro_control.c

某次运行程序:

./pro_control

进程控制 [fork() exec() wait() waitpid()]_第1张图片
Figure1. ./pro_control运行结果I

如果将代码中的sleep(1)注释去掉,重新编译并运行程序得到运行结果:

进程控制 [fork() exec() wait() waitpid()]_第2张图片
Figure2. ./pro_control运行结果II

[对fork创建子进程的理解]

在不去掉sleep(1)前的注释符号对应的可执行程序的运行结果也可能出现运行结果II的情况,因为当程序执行fork()语句后就又多出了一个进程,进程的运行符合“多个进程的运行方式”。

这个程序的运行过程如下:

进程控制 [fork() exec() wait() waitpid()]_第3张图片
Figure3. pro_control.c程序的运行过程

  • Parent进程在执行pid = fork()之前,不存在child进程。
  • Parent进程调用fork,这是一个系统调用,因此进入内核。
  • 因为调用了fork,内核根据父进程复制(说明两个进程的用户地址空间不一样)出一个子进程,父进程和子进程的PCB(进程控制块)信息相同,用户态代码和数据也相同(父进程与子进程的不同之处可用man fork查看)。现在,子进程的状态看起来和父进程一样,做完了初始化且刚调用fork进入了内核(实际上只有parent做了初始化和调用了fork),Parent和child进程都在等待从内核返回(fork调用了一次,会回两次的含义)
  • 除parent和child进程在等待从内核返回外,可能还有很多别的进程也等待从内核返回。那么,是parent还是child进程先返回或者是两者都要等待,都不一定,在有多个进程的情况下,哪个进程能先返回运行取决于内核的调度算法。
  • 如果某个时刻parent进程先从内核返回,fork()在父进程中返回其所建子进程的id(是一个大于0的整数),如果parent进程不再有阻塞状态且没有优先级更高的进程就绪运行,则parent进程会将后面的代码执行完毕并结束parent进程,而此时child进程还未从内核返回,当parent进程结束时,parent的父进程shell认为它所调用的进程都结束了,于是打印shell界面的提示符,而事实上child进程还未结束,当child进程从内核中返回时,fork在child子进程中的返回值为0,child进程开始执行后续代码,这就是child进程打印字符串打到shell提示符后面的缘故,得到运行结果I的情形。如果parent进程运行时又因内核调度而重新回到阻塞状态,那么此时child进程就可能比parent先进入运行状态而将child进程运行结束后parent进程才进入运行状态,或者child未运行完毕也进入阻塞状态导致两者交替运行,这就成了运行结果II的情形。当在进程中加入sleep(1)时,每个进程执行到sleep(1)时都会进入休眠状态等待从内核中返回,这时另外一个进程就有了更大的可能进入运行状态,于是造就了运行结果II的输出情况。
  • 如果某个时刻child子进程先从内核中返回,fork在child进程中的返回值为0。两个进程的运行情况跟parent先从内核返回的运行情况相似。

2 在进程中调用exec()

(1) EXEC(3)  LinuxProgrammer’s  Manual

[头文件及原型]

#include <unistd.h>

 

int execl(const char *path, const char *arg, ...);

int execlp(const char *file, const char *arg, ...);

int execle(const char *path, const char *arg, ..., char *const envp[]);

int execv(const char *path, char *const argv[]);

int execvp(const char *file, char *const argv[]);

int execve(const char *path, char *const argv[], char *const envp[]);

[功能简述]

当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行


[返回值]

这些函数如果调用成功则加载新的程序从启动代码开始执行,不再返回,如果调用出错则返回-1,所以exec函数只有出错的返回值而没有成功的返回值。


[exec函数后缀]

不带字母p(表示path)的exec函数第一个参数必须是程序的相对路径或绝对路径,例如"/bin/ls"或"./a.out",而不能是"ls"或"a.out"。对于带字母p的函数:如果参数中包含/,则将其视为路径名。否则视为不带路径的程序名,在PATH环境变量的目录列表中搜索这个程序。


带有字母l(表示list)的exec函数要求将新程序的每个命令行参数都当作一个参数传给它,命令行参数的个数是可变的,因此函数原型中有...,...中的最后一个可变参数应该是NULL,起sentinel的作用。对于带有字母v(表示vector)的函数,则应该先构造一个指向各参数的指针数组,然后将该数组的首地址当作参数传给它,数组中的最后一个指针也应该是NULL,就像main函数的argv参数或者环境变量表一样。


对于以e(表示environment)结尾的exec函数,可以把一份新的环境变量表传给它,其他exec函数仍使用当前的环境变量表执行新程序。


(2) 单独验证exec功能

[1] exec_call.c

#include <stdio.h>

int main(void)
{
	printf("hello, world\n");
	return 0;
}

gcc -o  exec_call  exec_call.c


[2] exec_test.c

#include <unistd.h>
#include <stdio.h>

int main(void)
{
	char *const argv[] ={"exec_call", NULL};
	execv("./exec_call", argv);
     printf(“here here\n”);
	return 0;
}

gcc -g  -o  exec_test exec_test.c


[3] gdb单步调试

lly@debian:~/mydir/lly_books/linux_c_programming_osl/system_program/progress$gdb exec_test

GNU gdb (GDB) 7.4.1-debian

Copyright (C) 2012 Free SoftwareFoundation, Inc.

License GPLv3+: GNU GPL version 3 orlater <http://gnu.org/licenses/gpl.html>

This is free software: you are free tochange and redistribute it.

There is NO WARRANTY, to the extentpermitted by law.  Type "showcopying"

and "show warranty" fordetails.

This GDB was configured as"i486-linux-gnu".

For bug reporting instructions, pleasesee:

<http://www.gnu.org/software/gdb/bugs/>...

Reading symbols from/home/lly/mydir/lly_books/linux_c_programming_osl/system_program/progress/exec_test...done.

(gdb) start

Temporary breakpoint 1 at 0x8048425:file exec_test.c, line 6.

Starting program: /home/lly/mydir/lly_books/linux_c_programming_osl/system_program/progress/exec_test

 

Temporary breakpoint 1, main () atexec_test.c:6

6        char  *const argv[] ={"exec_call", NULL};

(gdb) s

7        execv("./exec_call",argv);

(gdb) s

process 3964 is executingnew program:/home/lly/mydir/lly_books/linux_c_programming_osl/system_program/progress/exec_call

hello, world

[Inferior 1 (process 3964) exited normally]

(gdb) s

The program is not being run.

(gdb) quit

lly@debian:~/mydir/lly_books/linux_c_programming_osl/system_program/progress$
结合exec函数功能,由蓝色字体可以看出 当进程调用execv函数时,该进程的用户空间代码和数据完全被exec_call替换,从exec_call的启动例程开始执行 。调用execv进程后面的代码没有被执行。

(3) fork()进程中调用用execv

用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。


在1.2“fork创建子进程代码”中加入execv函数:

if (pid == 0) {
	msg	= "This is in child progress\n";
	n	= 6;
	char *const argv[] ={"exec_call", NULL};
	execv("./exec_call", argv);
}

重新编译程序,再次执行的结果:

This is in parent progress

hello, world

This is in parent progress

This is in parent progress
父进程和子进程执行顺序跟1.2中分析一样,但子进程中由于调用execv()函数后,子进程的用户空间和代码被exec_call替换。


3 wait() 和waitpid()函数

(1) WAIT(2) Linux Programmer’s Manual

[头文件及原型]

#include <sys/types.h>

#include <sys/wait.h>

 

pid_t  wait(int *status);

pid_t  waitpid(pid_t pid, int *status, int options);

[功能简述]

调用这两个函数的进程用这两个函数来等待其子进程状态的改变,并获取子进程所改变的信息。这里所指的状态的改变包括:子进程终止;子进程被一个信号终止来;子进程被一个信号恢复。对于一个已经终止的子进程,用wait能够让系统释放与子进程相关的资源;如果不用wait则终止的进程会变为僵尸进程。


一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait 或waitpid获取这些信息,然后彻底清除掉这个进程。


如果子进程状态已经改变,那么wait调用会立即返回。否则调用wait的进程将会阻塞直到有子进程改变状态或者有信号来打断这个调用。


[返回值]

若调用成功则返回清理掉的子进程id,若调用出错则返回-1。


[wait()和waitpid()区别]

  • 如果父进程的所有子进程都还在运行,调用wait将使父进程阻塞,而调用waitpid时如果在options参数中指定WNOHANG可以使父进程不阻塞而立即返回0。
  • wait等待第一个终止的子进程,而waitpid可以通过pid参数指定等待哪一个子进程。

关于参数的含义可用man wait查看。


(2) 在父进程中调用waitpid()

将1.2“fork()创建子进程”代码中子进程和父进程会运行的代码改为如下(并删掉打印消息的C语句):

	if (pid == 0) {
		char *const argv[] ={"exec_call", NULL};
		execv("./exec_call", argv);
	} else {
		int stat_val;
		
		waitpid(pid, &stat_val, 0);
		
		//Child  exit normally
		if ( WIFEXITED(stat_val) ){
			printf("Child exited with code %d\n", WEXITSTATUS(stat_val) );		
		} else if ( WIFSIGNALED(stat_val) ) {	//Child was terminated by a siganl 

			printf("Child terminated by signal %d\n", WTERMSIG(stat_val) );

		} else if ( WIFSTOPPED(stat_val) ) {	//Child was stopped by a delivery siganl 
			printf("%d signal case child stopped\n", WSTOPSIG(stat_val) );
		} else if ( WIFCONTINUED(stat_val) ){	//Child was resumed by delivery SIGCONT
			printf("Child was resumed by SIGCONT\n");
		}


	}
waitpid()函数的pid和option参数分别为子进程ID号和0,让父进程阻塞等待子进程的结束。stat_val获取到关于子进程的 改变状态。并用if … else 一一核对,编译并运行程序得到以下结果:

hello, world

Child exited with code 0

在子进程被exec_call进程代替后,一切返回值由exec_call决定, 因为exec_call进程没有exit()一类的返回值,所以这个0是exec_call正常退出由main种的renturn 0;返回的值。


[2014.8.9 - 17.16  --- 2014.8.10 - 10.24]



shellforkexec函数

系统中同时运行着很多进程,这些进程都是从最初只有一个进程开始一个一个复制出来的。在Shell下输入命令可以运行一个程序,是因为Shell进程在读取用户输入的命令之后会调用fork复制出一个新 的Shell进程,然后新的Shell进程调用exec执行新的程序。


fork()

fork()函数属于系统调用,在一个进程中调用此函数时会发生以下过程:进入内核-->执行fork对应的内核代码-->从内核返回到用户空间。

  • 执行fork对应的内核代码:内核根据父进程复制出一个子进程,父进程和子进程的PCB信息相同id不同),用户代码和数据(如环境变量,程序数据)也相同。

  • 从内核返回:fork()函数从内核返回时,因为父子进程的PCB,用户代码和数据都相同,就使子进程也“似乎”调用了fork()函数,所以fork()函数会在父进程和子进程中都返回一次。fork()在父进程中返回子进程的id值,在子进程中返回0

exec函数

当进程调用一种exec函数时,如果调用成功则加载新的程序从启动代码开始执行,该进程的用户空间代码和数据完全被新程序替换。调用成功时调用exec函数的程序不再返回,如果调用出错则返回-1。所以exec函数只有出错的返回值而没有成功的返回值。用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。

[2014.9.20 -- 17.15]

LC Note Over.

你可能感兴趣的:(进程控制 [fork() exec() wait() waitpid()])