进程控制相关系统调用函数
fork
与vfork
fork
函数
系统调用fork()函数派生一个进程,函数原型为:
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
运行成功:
-
父进程返回子进程ID,子进程返回
0
; -
运行出错返回
-1
。
fork系统调用的作用是复制一个进程,从而出现两个几乎一样的进程。一般来说, fork后是父进程先执行还是子进程先执行是不确定的,取决于内核所实使用的调度算法。
fork函数示例,fork_test.c
:
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { int count = 0; pid_t pid; pid = fork(); if(pid < 0) { printf("error in fork!"); exit(1); } else if(pid == 0) printf("I am the child process, the count is %d, my process ID is%d\n", count, getpid()); else printf("I am the parent process, the count is %d, my process ID is%d\n", ++count, getpid()); return 0; }
编译后运行:
$ ./fork_test I am the parent process, the count is 1, my process ID is2308 I am the child process, the count is 0, my process ID is2309
在语句pid = fork();
之前,只有一个进程在执行代码,但在该语句之后,
有两个进程在执行之后的代码,根据pid的不同执行不同的语句。
fork调用的神奇之处在于被调用一次,能够返回两次,返回结果可能有3种情况:
- 父进程中:fork返回新创建的子进程的ID
- 子进程中:fork返回0
- 出现错误:fork返回负值
fork出错的原因有2:
-
当前进程数已达系统规定的上限,此时errno的值被设置为
EAGAIN
-
系统内存不足,此时
errno
的值被设置为ENOMEN
errno是Linux下的一个宏定义常量,当Linux中C API函数发生异常时, 一般会将errno变量赋值为一个正整数(需include),不同的值表示不同的含义, 通过查看该值可推测出错原因。
vfork
函数
vfork()与fork()的区别是:
- fork()需要复制父进程的数据段,而vfork()不需要完全复制,
- 在子进程调用exec()或exit()之前,子进程与父进程共享数据段。
- fork()不对父子进程的执行次序作限制,而vfork()调用后,子进程先运行, 父进程挂起,直到子进程调用了exec()或exit()后,父子进程的执行次序才不再有限制。
实际上,vfork()创建出的不是真正意义的进程,它缺少了进程4要素的最后一项——独立的内存资源。
vfork()创建父子进程共享数据段测试,vfork_test1.c()
:
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { int count = 1; int child; printf("Before create son, the father's count is:%d\n", count); child = vfork(); if(child < 0) { printf("error in vfork!"); exit(1); } if(child == 0) { printf("This is son, his pid is:%d and the count is:%d\n", getpid(),++ count); exit(1); } else printf( "After son, This is father, his pid is:%d and the count is:%d, and the child is:%d\n", getpid(), count, child); return 0; }
编译后运行:
$ ./vfork_test1 Before create son, the father's count is:1 This is son, his pid is:2530 and the count is:2 After son, This is father, his pid is:2529 and the count is:2, and the child is:2530
可以看出,在子进程中修改了count
的值,变为2
,而父进程中count
值也为2
,
说明父子进程共享count
,即父子进程共享内存区。
vfork()创建子进程导致父进程挂起测试,vfork_test2():
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { int count = 1; int child; printf("Before create son, the father's count is:%d\n", count); if(!(child = vfork())) { int i; for(i=0; i<100; i++) { printf("This is son, the i is:%d\n", i); if(i == 70) exit(1); } printf("This is son, his pid is:%d and the count is:%d\n", getpid(), ++count); exit(1); } else printf( "After son, This is father, his pid is:%d and the count is:%d, and the child is:%d\n", getpid(), count, child); return 0; }
编译后运行:
$ ./vfork_test2 Before create son, the father's count is:1 This is son, the i is:0 This is son, the i is:1 This is son, the i is:2 ...省略 This is son, the i is:67 This is son, the i is:68 This is son, the i is:69 This is son, the i is:70 After son, This is father, his pid is:2541 and the count is:1, and the child is:2542
可以看出,父进程是等待子进程执行完毕后才开始执行。
exec
函数
Linux使用exec函数族来执行新的程序,以新的子进程来完全代替原有的进程, exec函数族包含6个函数:
#include <unistd.h> int execl(const char *pathname, const char *arg, ...); int execlp(const char *filename, const char *arg, ...); int execle(const char *pathname, const char *arg, ..., char *const envp[]); int execv(const char *pathname, char *const argv[]); int execvp(const char *filename, char *const argv[]); int execve(const char *pathname, char *const argv[], char *const envp[]);
运行成功无返回,出错返回-1
。
-
函数中含义字母
l
的:其参数个数不定,参数由命令行参数列表组成, 最v后一个NULL
表示结束。 -
函数中含义字母
v
的:使用一个字符串数组指针argv
指向参数列表, 与含字母l的函数参数列表完全相同。 -
函数中含义字母
p
的:可以自动在环境变量PATH
指定的路径中搜索要执行的程序, 其第一参数filename
为可执行函数的文件名, 注意其它函数的第一个参数pathname
为路径名 -
函数中含义字母
e
的:比其它函数多了一个字符串指针型的envp
参数,用于指定环境变量。
实际上,只有execve()
函数才是真正意义上的系统调用,
其它都是在此基础上经过包装的库函数。与一般情况不同,
exec函数族执行成功后不会返回,因为调用进程实体,包括代码段、
数据段和堆栈段都被新的内容取代,只是进程ID等一些表面上的信息仍保持原样。
exec函数族使用举例,exec_example.c
:
#include <unistd.h> #include <stdio.h> int main(void) { char *envp[] = {"PATH=/tmp", "USER=root", "STATUS=testing", NULL}; char *argv_execv[] = {"echo", "excuted by execv", NULL}; char *argv_execvp[] = {"echo", "excuted by execvp", NULL}; char *argv_execve[] = {"env", NULL}; if(fork()==0) { if(execl("/bin/echo", "echo", "executed by execl", NULL)) perror("Err on execl"); } if(fork()==0) { if(execlp("echo", "echo", "executed by execlp", NULL)) perror("Err on execlp"); } if(fork()==0) { if(execle("/usr/bin/env", "env", NULL, envp)) perror("Err on execle"); } if(fork()==0) { if(execv("/bin/echo", argv_execv)) perror("Err on execv"); } if(fork()==0) { if(execvp("echo", argv_execvp)) perror("Err on execvp"); } if(fork()==0) { if(execve("/usr/bin/env", argv_execve, envp)) perror("Err on execve"); } return 0; }
上述程序用到了perror()
函数,它用来将函数发生错误的原因输出到标准输出stderr
,
其函数原型为:
code-block include void perror(const char *s)
编译后执行:
$ ./exec_example PATH=/tmp USER=root STATUS=testing executed by execl executed by execlp $ PATH=/tmp USER=root STATUS=testing excuted by execvp excuted by execv
由于各子进程执行的顺序无法控制,因而每次运行结果的输出顺序会有不同。
使用exec函数族,一般要加上错误判断语句,因为exec函数易由多种原因运行失败:
-
找不到文件或路径:
errno
被设置为ENOENT
-
数组
argv
和envp
忘记使用NULL
结束:errno
被设置为EFAULT
-
没有文件的运行权限:
errno
被设置为EACCES
exit
与_exit
函数
这两个函数都是用于终止进程,其定义分别为:
#include <stdlib.h> void exit(int status); #include <unistd.h> void _exit(int status);
两者主要区别在于:
- 定义及所需头文件不同
-
_exit()
立即进入内核;exit()
则先执行一些清除处理 (包括调用执行个终止处理程序,关闭所有标准I/O流等),然后进入内核。 -
exit()
在调用之前要检查文件的打开情况,把文件缓冲区的内容写回文件;_exit()
则直接使进程停止,清除其使用的内存空间,并销毁其在内核中的各种数据结构。
在Linux的标准函数库中,有一套被称为「高级I/O的函数」,如:printf()
、fopen()
等,
也被称为「缓冲I/O(buffered I/O)」,其特征是对应每一个打开的文件,
在内存中都有一片缓冲区,每次会多读出若干条记录,当达到一定的条件(如达到一定数量,
或遇到特定字符,如\n
和文件结束符EOF
)时,再将缓冲区的内容一次性写入文件,
从而增加读写速度。但是,这种情况下,如果使用_exit()
退出,会导致某些数据未被保存,
而用exit()
则不会有问题。
exit()
与_exit()
函数的区别测试,exit_differ.c
:
#include <sys/types.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { pid_t pid; if((pid=fork()) == -1) { printf("failed to create a new process\n"); exit(0); } else if(pid == 0) { printf("\nchild process, output begin\n"); printf("child process, content in buffer"); _exit(0); } else { printf("parent process, output begin\n"); printf("parent process, content in buffer"); exit(0); } return 0; }
编译后执行:
$ ./exit_differ parent process, output begin parent process, content in buffer child process, output begin
由于printf函数遇到\n
时才从缓冲区读取数据,在子进程中,
因为_exit(0)
直接将缓冲区的内容清除了,内容没有显示;
而父进程中,执行exit(0)
之前会先将缓冲区的内容显示出来。
wait
与waitpid
函数
在一个进程调用了exit()
之后,该进程并非立即消失,而是留下一个僵尸进程(Zombie)
的数据结构,这时的一种处理方法就是使用wait()
和waitpid()
函数。
僵尸态是进程的一种特殊状态,没有任何可执行代码,也不能被调度, 仅仅在进程中保留一个位置,记载改进程的退出状态等信息供其它进程收集。
wait()
和waitpid()
函数原型:
#include <sys/types.h> #include <sys/wait.h> pid_t wait(int *status); pid_t waitpid(pid_t, int *status, int options);
运行成功返回进程ID,出错返回-1
。
-
参数
status
:用于保存进程退出时的一些状态,如果只是想把进程灭掉, 可以设置该参数为NULL
。 -
参数
pid
:用于指定所等待的线程。
pid取值 | 含义 |
---|---|
pid > 0
|
只等待进程ID为pid 的子线程
|
pid = -1
|
等待任何一个子线程,此时waitpid 等价于wait
|
pid = 0
|
等待同一个进程组中的任何子进程 |
pid < -1
|
等待一个指定进程组中的任何子进程,其进程ID为pid的绝对值 |
-
参数
options
:提供一些额外的选项来控制waitpid
,包括WNOHANG
和WUNTRACED
两个选项, 这是两个常数,可以用|
运算符连接使用。其中WNOHANG
参数用于设置不等待子进程退出, 立即返回,此时waitpid
返回0
;WUNTRACED
参数用于配置跟踪调试。
进程一旦调用wait后,就立刻阻塞自己,如果当前进程的某个子进程已退出, 则收集其信息,否则wait会一种阻塞在这里,直到有一个僵死进程出现。
wait
示例
wait_example.c
:
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { pid_t pc, pr; if((pc = fork()) < 0) { printf("error in fork!"); exit(1); } else if(pc == 0) { printf("This is child process with pid of %d\n", getpid()); sleep(10); } else { pr = wait(NULL); printf("I catched a child process with pid of %d\n", pr); } exit(0); }
编译后执行:
$ ./wait_example This is child process with pid of 10093 I catched a child process with pid of 10093
可以看到,第1行输出后,等待大约10秒,第2行才输出,这10秒就是子线程的睡眠时间。
waitpid
示例
父进程和子进程分别睡眠10秒钟和1秒钟,代表所作的相应工作。
父进程利用工作的简短间歇查看子进程是否退出,如果退出就收集它。
waitpid_example.c
:
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { pid_t pc, pr; if((pc = fork()) == -1) { printf("failed to create a new process"); exit(0); } else if(pc == 0) { sleep(10); exit(0); } do { pr = waitpid(pc, NULL, WNOHANG); if(pr == 0) { printf("No chiled exited\n"); sleep(1); } } while (pr == 0); if (pr == pc) printf("successfully get child %d\n", pr); else printf("some error occured\n"); return 0; }
编译运行:
$ ./waitpid_example No chiled exited No chiled exited No chiled exited No chiled exited No chiled exited No chiled exited No chiled exited No chiled exited No chiled exited No chiled exited successfully get child 2711
可以看到,父进程经过10次失败尝试后,终于收集到了退出的子进程。
获取子进程返回状态
对于wait()
和waitpid()
中的status
参数,当其值不为NULL
时,
子进程的退出状态会以int值的形式保存其中,通过一套专门的宏(macro)
可以读取存入的状态值,这里只列举两个常用的宏:
宏定义 | 含义 |
---|---|
WIFEXITED(status)
|
子进程正常退出时,返回一个非零值,否则返回零 |
WEXITSTATUS(status)
|
当WIFEXITED 为真时,此宏才可用,返回该进程退出的代码
|
示例,子进程调用exit(3)
退出,WIFEXITED(status)
指示子进程正常退出,WEXITSTATUS(status)
就会返回3
。
get_status.c
:
#include <sys/types.h> #include <sys/wait.h> #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main(void) { int status; pid_t pc, pr; if((pc = fork()) < 0) { printf("error in fork!"); exit(1); } else if(pc == 0) { printf("This is child process with pid of %d.\n", getpid()); exit(3); } else { pr = wait(&status); if(WIFEXITED(status)) { printf("the child process %d exit normally.\n", pr); printf("the return code is %d.\n", WEXITSTATUS(status)); } else printf("the child process %d exit abnormally.\n", pr); } return 0; }
调试运行:
$ ./get_status This is child process with pid of 2718. the child process 2718 exit normally. the return code is 3.
可以看出,父进程捕捉到了子进程的返回值3
。
system
函数
函数原型:
#include <stdlib.h> int system(const char *cmdstring);
sysytem()
调用fork()
产生子进程,由子进程来调用
/bin/sh-cmdstring
来执行参数cmdstring
字符串所代表的命令,
此命令执行完后随即返回原调用的进程。
编程示例,4次调用system,设置不同的命令行参数,system返回不同的结果,
cmd_system.c
:
#include <stdio.h> #include <stdlib.h> int main(void) { int status; if((status = system(NULL)) < 0) { printf("system error!\n"); exit(0); } printf("exit status=%d\n", status); if((status = system("date")) < 0) { printf("system error!\n"); exit(0); } printf("exit status=%d\n", status); if((status = system("invalidcommand")) < 0) { printf("system error!\n"); exit(0); } printf("exit status=%d\n", status); if((status = system("who; exit 44")) < 0) { printf("system error!\n"); exit(0); } printf("exit status=%d\n", status); return 0; }
编译运行:
$ ./cmd_system exit status=1 2019年 12月 10日 星期二 14:55:36 CST exit status=0 sh: 1: invalidcommand: not found exit status=32512 deeplearning pts/0 2019-12-10 13:46 (192.168.1.110) exit status=11264
-
第1次调用system,参数为
NULL
,返回结果为1,说明在本Linux系统下system可用; -
第2次调用system,参数为
data
,system成功执行; - 第3次调用system,参数为一个非法的字符串命令,返回结果shell的终止状态(命令出错)32512;
-
第4次调用system,参数为
who
,显示登录用户情况,exit 44
是退出当前的shell,system成功返回,返回值11264。