UNIX环境编程(c语言)--多进程(二)--多进程编程

上一篇相关文章 :UNIX环境编程(c语言)–多进程(一)–基础知识

多进程编程

创建新进程 fork

一个进程可以使用fork函数创建一个新的子进程
原型

pid_t fork(void);
// 失败时返回 -1

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的情况

如果希望能够确定父进程和子进程的运行顺序,可以采用进程间通信,以后再更新相关内容


子进程可以在父进程得到的

除非特殊说明,以下都是复制一个副本而不是共享,父进程和子进程共享正文段(代码)

  1. 复制堆、栈、数据段的副本(不同空间,互不影响)
  2. 复制打开的文件描述符,且共享同一个文件表项(拥有同样的文件偏移量)
  3. 缓冲区数据
  4. 子进程会保持父进程的重定向
  5. 进程的资格(真实(real)/有效(effective)/已保存(saved) 用户号(UIDs)和组号(GIDs))
  6. 环境(environment)变量
  7. 堆栈
  8. 内存
  9. 打开文件的描述符(注意对应的文件的位置由父子进程共享, 这会引起含糊情况)
  10. 信号(signal)控制设定
  11. nice值 (注:nice值由nice函数设定,该值表示进程的优先级, 数值越小,优先级越高)
  12. 进程调度类别(注:进程调度类别指进程在系统中被调度时所属的类别,不同类别有不同优先级,根据进程调度类别和nice值,进程调度程序可计算出每个进程的全局优先级,优先级高的进程优先执行)
  13. 进程组号
    对话期ID(Session ID) (注:译文取自《高级编程》,指:进程所属的对话期 (session)ID, 一个对话期包括一个或多个进程组, 更详细说明参见《APUE》 9.5节)
  14. 当前工作目录
  15. 根目录 (根目录不一定是“/”,它可由chroot函数改变)
  16. 文件方式创建屏蔽字(file mode creation mask (umask))
  17. 资源限制
  18. 控制终端

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);
/*成功返回进程ID,失败返回0*/

作用:

  • 如果所有的子进程都还在运行,则阻塞
  • 如果一个子进程终止,返回终止信息
  • 没有任何子进程,出错返回

两个函数的区别如下:

  • 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, ... /* (char *)0 */ );
int execv(const *pathname, char *const argv[] );
int execle(const *pathname, const char *arg0, ... /* (char *)0 */,char *const envp[] );
int execve(const *pathname, char *const argv[] ,char *const envp[]);
int execlp(const *filename, const char *arg0, ... /* (char *)0 */ );
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");
//如果这里的 eth0、4.2.2.2 等是一个变量参数,我们则可以使用snprintf()格式化生成该命令:
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内

结果如下
在这里插入图片描述

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GuanFuXinCSDN

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值