上一篇相关文章 :UNIX环境编程(c语言)–多进程(一)–基础知识
多进程编程
创建新进程 fork
一个进程可以使用fork函数创建一个新的子进程 原型
pid_t fork(void);
fork函数被调用一次,会返回两次。给父进程返回的是子进程的ID,给子进程返回 0 。 子进程可以通过调用getpid和getppid得到本进程id和父进程id 但是父进程只能通过fork的返回得知子进程的id
如果fork一个子进程,当不希望父进程等待其退出,也不希望其成为僵死进程,可以调用两次fork
调用一次,返回两次怎么理解呢 fork创建子进程会复制父进程的进程数据、堆、栈、缓冲区等,然后在另一个进程环境执行子进程。这时候就相当于两个程序在运行了,父进程和子进程都会在fork之后的语句开始执行,在父进程那边fork返回的是子进程id(大于0),而在子进程那边fork返回的 0
我们来写一个程序来理解
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <errno.h>
4
5 int main(int argc, char **argv)
6 {
7 pid_t pid;
8
9 printf("PID[%d] \r\n",getpid());
10 pid = fork();
11 if(pid < 0)
12 {
13 printf("fork create child process fialure : %s \r\n",strerror(errno));
14 return -1;
15 }
16 else if(pid == 0)
17 {
18 printf("child process pid %d parent ppid %d \n",getpid(),getppid());
19 return 0;
20 }
21 else
22 {
23 printf("parent %d \n",getpid());
24 return 0;
25 }
26 }
程序解释
- 在第10行,调用了fork,这时候会复制了父进程的存储空间,创建了子进程在另一个运行空间运行,可以近似理解为:两个程序在运行
- 如果创建失败,pid会小于0,这说明子进程没有被创建
- 第16行,测试pid是否等于0,在父进程中fork返回的是子进程的id,而在子进程中for返回的是0,所以子进程将执行,父进程不执行
- 第21行,子进程在执行上一个判断条件后的语句后 return退出,不会进行往下执行,只有父进程执行到了这里
注意 : 这里的解释是按顺序父进程和子进程一起解释,但是实际运行时,父进程和子进程谁先运行是不确定的,这取决于内核的调度算法
我们来看运行运行结果 我将程序运行了5次,有的父进程为 1,有的父进程是一个大的数字 这就是因为父进程和子进程运行顺序不确定导致的
如果父进程先运行,然后return退出了,到子进程运行时,原本的父进程已经死了,这时候子进程会被init进程‘领养’,才出现了父进程为1的情况
如果希望能够确定父进程和子进程的运行顺序,可以采用进程间通信,以后再更新相关内容
子进程可以在父进程得到的
除非特殊说明,以下都是复制一个副本而不是共享,父进程和子进程共享正文段(代码)
- 复制堆、栈、数据段的副本(不同空间,互不影响)
- 复制打开的文件描述符,且共享同一个文件表项(拥有同样的文件偏移量)
- 缓冲区数据
- 子进程会保持父进程的重定向
- 进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs))
- 环境(environment)变量
- 堆栈
- 内存
- 打开文件的描述符(注意对应的文件的位置由父子进程共享, 这会引起含糊情况)
- 信号(signal)控制设定
- nice值 (注:nice值由nice函数设定,该值表示进程的优先级, 数值越小,优先级越高)
- 进程调度类别(注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级,优先级高的进程优先执行)
- 进程组号
对话期ID(Session ID) (注:译文取自《高级编程》,指:进程所属的对话期 (session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《APUE》 9.5节) - 当前工作目录
- 根目录 (根目录不一定是“/”,它可由chroot函数改变)
- 文件方式创建屏蔽字(file mode creation mask (umask))
- 资源限制
- 控制终端
vfork 函数
vfork的参数和返回值都与fork保持一致
但是vfork创建一个新进程的目的是调用exec去执行一个新的程序,而不是复制父进程,所以vfork不会去复制父进程的东西,而是等待调用exec
在调用exec之前,子进程在父进程的空间中运行,这时候如果试图改变变量的值,父进程的值也会改变,
vfork会保证子进程先运行,在调用exec或者exit之前,父进程处于阻塞的状态,这时需要注意,如果这两个函数之前有需要父进程完成的动作才能往下运行的话,就会导致死锁
虽然fork采用写时复制技术,但是vfork完全不复制还是会快上一些
如果没有调用exec或exit子进程就返回了,可能会导致未知的后果
exit 、_exit函数
作用:终止一个进程 main函数中的return也会调用exit,但是其他函数的return却不会调用
调用exit函数后,会先调用登记好的终止处理程序,最后通过fclose关闭所有打开流,这一点在上一篇文章中说明过如何登记终止处理函数
而调用_exit 或 _Exit 将会直接退出,包含调用终止处理函数和关闭打开流冲洗缓冲区
当进程的最后一个线程在启动例程中调用return时,线程的返回值不会用作进程的返回值值,进程以终止状态0退出
当最后一个线程调用pthread_exit 时,进程的终止状态也是0,与给pthread_exit 的参数无关
一个进程终止了后,其父进程没有对其进行善后处理(获得终止信息,释放资源),将成为僵死进程
wait 和waitpid函数
原型
pid_t wait(int *staloc);
pid_t waitpid(pid_t pid, int *staloc, int options);
作用:
- 如果所有的子进程都还在运行,则阻塞
- 如果一个子进程终止,返回终止信息
- 没有任何子进程,出错返回
两个函数的区别如下:
- wait在一个子进程终止前会阻塞,waitpid可以选择阻塞还是不阻塞
- waitpid可以指定等待的是哪个进程
参数说明:
- 两个函数的staloc,是用来接收终止状态的整型指针
- pid = -1时,waitpid等待任一个进程与wait一致
- pid > 0 ,等待进程id与pid相等的子进程
- pid = 0 ,等待组id和调研进程的组id一致的进程
- pid < 0,等待组id和pid的绝对值相等的子进程
options 参数可以是0,或者以下选项的位或结果
常量 | 说明 |
---|
WCONTINUED | 子进程停止后继续,报告状态 | WNOHANG | 不阻塞 | WUNTRACED | 子进程停止,状态未报告过,则报告 |
当一个进程终止时,内核会向其父进程发送SIGCHLD信号,系统默认动作是忽略这个信号
waitid,wait3,wait4
也是获取进程终止状态的函数,这里不再展开,可以使用命令man查看
exec 函数
当进程调用exec函数后,该进程执行的程序将完全替换为新程序,替换了当前进程的正文段、数据段、堆栈(进程id不变),而新程序在main开始执行
exec函数有七个,作用一致,就是参数列表不一样
int execl(const *pathname, const char *arg0, ... );
int execv(const *pathname, char *const argv[] );
int execle(const *pathname, const char *arg0, ... ,char *const envp[] );
int execve(const *pathname, char *const argv[] ,char *const envp[]);
int execlp(const *filename, const char *arg0, ... );
int execvp(const *filename, char *const argv[] );
int fexecve(int fd, char *const argv[] ,char *const envp[]);
函数名中, l 代表list,每个命令行参数都作为一个独立的参数,结尾有一个空指针 v 代表,命令行参数构造成一个字符串指针函数 e 代表,可以传一个环境的字符串指针数组,如果没有代表复制当前进程的环境 p 代表,使用文件名为参数,其他的采用路径名作为参数
采用文件名为参数时,如果包含/就当做目录查找,负责在PATH环境变量的目录中查找
exec将会丢掉父进程的文本段,也不会返回,如果返回了说明调用出错
七个函数只有execve是系统调用,其他几个关系如下
system 函数
在程序中执行一个命令字符串 原型
int system(const char *cmdstring);
在system内部实现中,实际上也是调用了fork exec waitpid。当三个函数都执行成功,将返回终止状态(与waitpid一致)
使用实例:将当前时间写入文件
system("ping -c 4 -I eth0 4.2.2.2");
char cmd_buf[256];
int count = 4;
char *interface="eth0";
char *dst_ip = "4.2.2.2";
snprintf(cmd_buf, sizeof(buf), "ping -c %d -I %s %s", count, interface, dst_ip);
system(cmd_buf);
需要注意的是,有SUID和SGID权限的用户,在调用system后,权限也将被保留下列,执行的新程序也将获得权限
对于特殊权限的程序,最好直接使用fork exec,并在之前更改权限
popen函数
功能也是执行一个命令字符串,但是可以返回执行的结果
原型
FILE *popen(const char *command, const char *type);
说明: popen()函数通过创建一个管道,调用fork()产生一个子进程,执行一个shell以运行命令来开启一个进程。这个管道必须由pclose()函数关闭,而不是fclose()函数。pclose()函数关闭标准I/O流,等待命令执行结束,然后返回shell的终止状态。如果shell不能被执行,则pclose()返回的终止状态与shell已执行exit一样。
参数type 可使用 "r"代表读取,"w"代表写入。 type参数只能是读或者写中的一种,得到的返回值(标准I/O流)也具有和type相应的只读或只写类型。如果type是"r"则文件指针连接到command的标准输出;如果type是"w"则文件指针连接到command的标准输入。
command参数是一个指向以NULL结束的shell命令字符串的指针。这行命令将被传到bin/sh并使用-c标志,shell将执行这个命令。
popen()的返回值是个标准I/O流,必须由pclose来终止。前面提到这个流是单向的(只能用于读或写)。向这个流写内容相当于写入该命令的标准输入,命令的标准输出和调用popen()的进程相同;与之相反的,从流中读数据相当于读取命令的标准输出,命令的标准输入和调用popen()的进程相同。
实例
FILE * p_file = NULL;
p_file = popen("ifconfig eth0", "r");
if (!p_file) {
fprintf(stderr, "Erro to popen");
}
while (fgets(buf, BUF_SIZE, p_file) != NULL) {
fprintf(stdout, "%s", buf);
}
pclose(p_file);
多进程编程实例
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <errno.h>
4 #include <sys/types.h>
5 #include <sys/stat.h>
6 #include <fcntl.h>
7 #include <string.h>
8
9 int g_var = 6;
10 char g_buf[] = "A string write to stdout.\n";
11
12 int main(int argc, char **argv)
13 {
14 pid_t pid;
15 int var = 88;
16 int fd = -1;
17 FILE *p_file = NULL;
18 char buf[1024];
19
20 if( write(STDOUT_FILENO,g_buf,sizeof(g_buf)) < 0)
21 {
22 printf("wirte error : %s\n",strerror(errno));
23 return -1;
24 }
25 if((fd = open("TEST_FORK", O_RDWR|O_CREAT|O_TRUNC, 0666)) < 0)
26 {
27 printf("open error : %s \n", strerror(errno));
28 return -2;
29 }
30
31 printf("befor fork\n");
32
33 pid = fork();
34 if(pid < 0)
35 {
36 printf("fork create child process fialure : %s \r\n",strerror(errno));
37 return -3;
38 }
39 else if(pid == 0)
40 {
41 printf("child process pid %d parent ppid %d \n",getpid(),getppid());
42 g_var++;
43 var++;
44 printf("g_var = %d , var = %d \n",g_var ,var);
45
46 execl("/sbin/ifconfig", "ifconfig", "eth0", NULL);
47
48 }
49 else if(pid > 0)
50 {
51 printf("parent %d \n",getpid());
52 printf("g_var = %d , var = %d \n",g_var ,var);
53 sleep(5);
54
55 p_file = popen("ifconfig eth0","r");
56 if(!p_file)
57 {
58 printf("popen error :%s \n", strerror(errno));
59 return -5;
60 }
61
62 while(fgets(buf, sizeof(buf), p_file) != NULL)
63 {
64 printf(" %s ",buf);
65 write(fd, buf, strlen(buf));
66 }
67
68 pclose(p_file);
69 close(fd);
70 }
71 return 0;
72 }
73
分析
-
第20行,我们使用了write系统调用不管是否有重定向,13行的输出会立刻输出到标准输出里; -
而第31行,使用print打印,在标准输出时是行缓冲的,重定向到文件后变成全缓存 也就是说,如果我们重定向到文件后,程序在fork之前,缓冲区还有数据还没有打印,这时候也会一起复制给子进程,那么befor fork将打印两次
-
第39行,判断fork返回0,也就是子进程内才会运行,在判断语句内,给两个值+1后打印(这时不会影响父进程中的值),然后调用execl,并将ifconfig的结果打印到标准输出,子进程到这里就结束了,因为execl函数是不会返回的 -
第49行,判断fork返回大于0,也就是只有父进程才运行,这里使用popen运行ifconfig命令后通过管道返回,并printf到标准输出,和写到文件TEST_FORK内
结果如下
|