Linux程序设计—多进程编程
文章目录
- 1、进程
- 1.1、创建进程
- 1.1.1、fork()
- 1.1.2、vfork()
- 1.2、执行进程——exec函数族
- 1.3、进程退出
- 1.3.1、exit()和_exit()
- 1.4、进程回收
- 1.4.1、僵尸进程
- 1.4.2、wait()
- 1.4.3、waitpid()
- 2、写在最后
1、进程
进程的定义:
进程是程序处于一个执行环境中在一个数据集上的一次运行过程,它是系统进行资源分配和调度的一个独立单位。每个进程都有自己独立的系统资源,一个进程中可以有多个线程,系统是系统资源分配的基本单位。
1.1、创建进程
整个Linux操作系统都是由父子进程结构组成,每个进程都有创建者,也就是父进程,但是有一个进程例外,也就是init进程,其为系统启动初始化后执行的第一个进程。
1.1.1、fork()
函数原型:
#include <unistd.h>
pid_t fork(void); //pid_t等价于有符号整型
主要作用: 创建一个子进程,这也就代表着,父进程可通过调用该函数创建一个子进程,父子进程各自独立,拥有自己的PCB,内存用户区,临时资源等,各自独立参与CPU调度
返回值:pid_t
类型的变量,一共有两个返回值(父进程返回一个,子进程返回一个)
细节探究:
fork
函数执行的流程:(1)调用_CREATE
函数,也就是进程创建部分,子进程进行虚拟地址申请,在子进程的内核空间进行不完全拷贝(2)调用_CLONE
函数,向父进程拷贝必要资源,子进程的用户空间进行完全拷贝,子进程继承所有父进程资源,如临时堆栈拷贝,代码完全拷贝(3)子进程执行fork
函数剩余部分,执行最后这个语句,fork
函数就会有二次返回,如果成功返回0,不成功返回1。不成功的主要原因有:
a.系统内存不够 b.进程表满(容量一般为200~400)c.用户的子进程太多(一般不超过25个)
所以fork
函数的返回值情况如下:
父进程调用fork()
,返回子进程pid(>0)
子进程调用fork()
,子进程返回0,调用失败的话就返回-1
这也就说明了fork
函数的返回值是2个
fork
的应用场景:一个父进程希望复制自己,使父进程和子进程执行不同的代码段。在网络编程中常用到fork
函数,例如:父进程等待客户端的服务请求,当请求到达时,父进程调用fork
,使子进程处理此请求。父进程则继续等待下一个服务请求。示例程序如下所示(仅展示关键代码):
void test_fork()
{pid_t pid;int data;while(1){printf("please input command number:");scanf("%d", &data);if(1 == data){pid = fork();if(pid > 0){}else if(0 == pid){while(1){printf("do net require, pid = %d\n", getpid()); //模拟子进程处理请求sleep(3);}}else{perror("create process failed\n"); //创建进程失败exit(-1);}}else{printf("isn't an excepted number\n");}}
}
执行该段代码编译生成的可执行文件可得到如下结果:
父进程执行时一直在等待用户输入数据,当用户输入1时,创建子进程(pid为1785),并建立网络链接,父进程则继续等待用户输入下一个数据,并根据输入的具体值完成指定的操作。
总结:父子进程都执行fork
函数,但执行不同的代码段,获取不同的返回值。创建出来的子进程在继承了所有的父进程资源后会从代码的fork()
处继续向下执行,该特性可被如下的程序段所验证:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{pid_t pid;printf("aaaaaa process pid = %d\n", getpid());pid = fork();printf("bbbbbb process pid = %d\n", getpid());while(1){sleep(1);}return 0;
}
执行该段代码编译生成的可执行文件可得到如下结果:
不难看出,父进程执行的代码段打印了"aaaaaa"和"bbbbbb"这两串字符串,而子进程仅打印了"bbbbbb"这一串字符串,即可验证上文所述。
1.1.2、vfork()
函数原型:
#include <unistd.h>
pid_t vfork(void); //pid_t等价于有符号整型
主要作用: 创建一个子进程,作用与
fork
函数一致
细节探究:
vfork
函数的调用序列和返回值与fork
相同,但二者的语义不同
(1)vfork
直接使用父进程存储空间,不拷贝(可将存储空间理解为联合体,每个成员均为vfork
函数创建的子进程,所有子进程共用同一段内存)
(2)vfork
保证子进程先运行,在子进程调用exit
或exec函数族
之后父进程才会被调度执行,如果在子进程退出之前子进程依赖父进程的进一步动作,则会导致死锁。下文程序段可验证vfork
函数的这一特性(仅展示关键代码):
void test_vfork()
{pid_t pid;int cnt = 0;pid = vfork();if(pid > 0){while(1){printf("i am a parent process\n");sleep(1);}}else if(0 == pid){while(1){printf("i am a child process\n");cnt++;sleep(1); //在子进程占用一段运行时间后,主动结束子进程if(3 == cnt){cnt = 0;exit(0);}}}else{perror("create process failed\n");exit(-1);}
}
执行该段代码编译生成的可执行文件可得到如下结果:
父进程调用vfork
函数创建子进程,先运行子进程,在合适的条件下子进程退出后父进程得以执行。
1.2、执行进程——exec函数族
函数原型: Linux下的exec函数族
是6个以exec
开头的函数,具体如下:
#include <unistd.h>int execl(const char *path, const char *arg, ...); //arg...传递给执行的程序的参数列表
int execv(const char *path, char *const argv[]); //arg...封装成指针数组的形式传递
int execle(const char *path, const char *arg, ..., char *const envp[]); //使用默认的环境变量(environment),在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[]);
主要作用: 通过
fork()
或者vfork()
函数创建子进程后,子进程几乎复制了父进程的全部内容,如果我们想要父子进程执行的内容不同,可以通过exec函数族
实现。exec函数族
提供了一个在进程中执行另一个程序的方法,它可以根据指定的文件名或目录名找到可执行文件,并用它来取代当前进程的数据段,代码段和堆栈段。在执行完后,当前进程除了进程号外,其它内容都被替换。
细节探究:
日常开发中,exec函数族
最常被用到的函数是:
int execl(const char *path, const char *arg, ...);
execl
的示例程序如下所示(仅展示关键代码):
func.c
void test_exec()
{pid_t pid;pid = fork();if(pid > 0){printf("i am a parent process\n");}else if(0 == pid){printf("i am a child process\n");if(execl("/home/pi/project/process_1/subprocedure", "subprocedure", NULL) < 0){perror("execl failed\n");exit(1); //exec调用失败,主动结束子进程}}else{perror("create process failed\n");exit(-1);}printf("end mark point\n");
}
subproc.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{int cnt = 0;while(cnt < 3){printf("child process execute subprocedure\n");cnt++;sleep(3);}exit(0);
}
执行该段代码编译生成的可执行文件可得到如下结果:
可以发现,父进程剩下的部分,子进程并没有执行。子进程在调用exec函数族
后,就只执行subproc.c
编译出来的可执行文件subprocedure
。通过exec函数族
实现了父子进程执行不同的内容。
特别注意:
(1)确保形参格式正确:如果exec函数族
的第一个形参为path
,则必须以“/”开头,否则将视其为文件名;path
需要为完整的文件目录路径,即被执行的文件也需要被包含在path
中。如果第一个形参为file
则会自动在path
中搜索
(2)要判断exec
是否执行成功,如果执行失败应结束该子进程。一种执行失败的情况如下所示:
这种情况是path路径的末尾未包含可执行文件所导致,exec
执行失败,结束子进程。
1.3、进程退出
进程常见的退出方法主要有以下三种:
(1)main
函数中调用return
实现进程退出,main
函数对应的进程的状态信息将会自动被系统中的特定进程所回收
(2)ctrl+c
中断正在运行的进程(信号机制)
(3)任何进程调用exit()
或_exit()
实现进程退出
1.3.1、exit()和_exit()
函数原型:
#include <stdlib.h>
void exit(int status);#include <unistd.h>
void _exit(int status);
主要作用: 两个函数功能和用法是一样的,都能结束调用此函数的进程
参数:status
为进程退出时的一个状态信息,ANSIC标准要求使用值0或宏EXIT_SUCCESS
指示程序正常终止,使用值1或宏EXIT_FAILURE
指示程序异常终止
细节探究:
exit()
和_exit()
最主要的区别有以下几点:
(1)exit()
属于标准库函数(标准c库中的函数),_exit()
属于系统调用函数(Linux系统中的函数)。使用时,两个函数所包含的头文件不一样
(2)exit()
在执行时,系统会检测进程打开文件情况,并将处于文件缓冲区的内容写入到文件当中再退出。而_exit()
则直接退出,不会将缓冲区的内容写入文件
下文给出的程序段可以验证exit()
和_exit()
之间的区别:
测试函数test()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{printf("test begin\n");printf("output string buffer");exit(0); //在main函数中等价于return 0printf("test end\n");//return 0;
}
执行该段代码编译生成的可执行文件可得到如下结果:
测试函数_test()
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>int main()
{printf("test begin\n");printf("output string buffer");_exit(0); //在main函数中等价于return 0printf("test end\n");//return 0;
}
执行该段代码编译生成的可执行文件可得到如下结果:
printf()
使用缓存I/O的方式,该函数在遇到'\n
时自动从缓冲区中将记录读出。而exit()
也能将文件缓冲区的内容写入到文件中再退出。所以不难看出两个运行结果的差异:执行exit()
代码段的第二行信息能被打印,而执行_exit()
代码段的第二行信息不能被打印。如果第一行输出也没有格式化控制符\n
,则_exit()
也不会将其打印出来。
1.4、进程回收
1.4.1、僵尸进程
僵尸进程简介: 在Linux中,正常情况下,子进程是通过父进程创建的,子进程再创建新的进程。子进程的结束和父进程的运行是一个异步过程。即父进程永远也无法预测子进程到底什么时候结束。当一个子进程完成它的工作退出之后,它的父进程需要采用合适的手段对子进程实现资源回收,否则就会产生僵尸(Zombie)进程。
僵尸进程危害:
(1)僵尸进程会造成一定的资源浪费,占用不必要的资源。任何一个子进程(init除外)在退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。但是仍然会保留一些信息(包括进程号,退出状态,运行时间等),称为僵尸进程的数据结构。
(2)当进程id达到了最大值的时候,因为有僵尸进程占用了部分进程id,使得无法再打开新的进程。
僵尸进程产生示例:
void test_zombie()
{pid_t pid;pid = fork();if(pid > 0){sleep(1); //延时片刻,保证子进程先运行printf("i am a parent process\n");while(1); //父进程保持不退出}else if(0 == pid){printf("i am a child process\n"); //子进程直接退出}else{perror("create process failed\n");exit(-1);}printf("end mark point\n");
}
执行该段代码编译生成的可执行文件,并另一个终端使用指令ps -ajx
查看,可得到如下结果:
进程状态为Z或Z+的进程即为僵尸进程
僵尸进程避免:
实际开发中有多种手段避免僵尸进程的出现,如子进程退出时向父进程发生SIGCHILD
信号,多次fork()
,将子进程变成孤儿进程等。下文着重讲解的,也是最常用的一种方法,是调用wait()
/waitpid()
函数,使子进程退出时被父进程回收。
1.4.2、wait()
函数原型:
#include <sys/types.h>
#include <sys/wait.h>pid_t wait(int *status);
主要作用: 父进程调用
wait()
阻塞等待子进程退出,并回收子进程的状态信息
参数:*status
指向的整型对象来保存子进程结束时的状态,即exit()
的形参值。此外,子进程的结束状态也可以有一些特定的宏来测定
返回值: 若成功回收子进程则返回子进程的进程号,失败则返回-1
细节探究:
使用wait()
回收进程时应该注意以下几点:
(1)如果子进程没有结束,则父进程会阻塞等待,无法向前推进,直到子进程结束。如果父进程占用某个临界资源,则容易出现死锁现象
(2)wait()
一次只能回收一个子进程,如果创建了多个子进程,则哪个子进程先结束就先被回收
(3)形参*status
可以为NULL,表示直接释放子进程PCB,不接收返回值
wait()
的示例程序如下所示(仅展示关键代码):
void test_wait()
{pid_t pid;pid = fork();if(pid > 0){pid_t retval;int status;printf("parent process is waiting...\n");retval = wait(&status); //阻塞等待子进程退出printf("parent process wait done, retval = %d status = %d\n", retval, status); //打印已回收子进程的进程号和结束状态}else if(0 == pid){int cnt = 0;while(cnt < 3){cnt++;printf("child process is running\n");sleep(3);}exit(0); //子进程退出}else{perror("create process failed\n");exit(-1);}
}
执行该段代码编译生成的可执行文件可得到如下结果:
父进程创建子进程之后阻塞等待子进程的退出,当子进程退出后父进程将回收子进程的状态信息。
1.4.3、waitpid()
函数原型:
#include <sys/types.h>
#include <sys/wait.h>pid_t waitpid(pid_t pid, int *status, int option);
主要作用: 同
wait
一致,等待子进程退出并回收子进程的状态信息
参数:
pid
:主要有两种情况,pid
= -1表示回收任何一个子进程,pid
> 0表示回收对应pid
的任一子进程
status
:与wait()
中的参数status
作用一致
option
:指定回收方式,常见的为0(阻塞等待子进程结束)或WNOHANG
(非阻塞等待)
返回值: 若成功回收子进程则返回子进程的进程号,失败则返回-1,返回0表示option
为WNOHANG
且没有子进程退出
细节探究:
waitpid()
的作用和wait()
一样,但它并不一定等待第一个结束的子进程。waitpid()
提供了若干选项,可以实现非阻塞的进程回收功能。事实上在Linux内部实现wait()
时直接调用的就是waitpid()
,可以将wait()
理解成waitpid()
的一个特例。下文代码展示了两个函数之间的等效关系:
//retval = wait(&status);
retval = waitpid(-1, &status, 0); //阻塞等待子进程退出
用waitpid()
去跑上文wait()
示例程序可得到一样的结果,如下图所示:
下文给出的程序段是验证waitpid()
的非阻塞回收功能(仅展示关键代码):
void test_waitpid()
{pid_t pid;pid = fork();if(pid > 0){sleep(1); //睡眠一段时间,确保子进程先运行pid_t retval;int status;printf("parent process is waiting...\n");if((retval = waitpid(-1, &status, WNOHANG)) == 0){printf("parent process didn't wait child process exit\n"); //非阻塞等待子进程退出,回收成功}else{printf("parent process wait done, retval = %d status = %d\n", retval, status); //回收失败}while(1); //父进程不能退出,否则子进程会成为孤儿进程,由init进程完成状态收集工作}else if(0 == pid){int cnt = 0;while(cnt < 4){cnt++;printf("child process is running\n");sleep(2);}exit(0);}else{perror("create process failed\n");exit(-1);}
}
执行该段代码编译生成的可执行文件可得到如下结果:
可以发现,父进程未能成功回收子进程,子进程在退出后成为僵尸进程,在另一个Linux终端输入命令ps -ajx
可观察到这一僵尸进程。
若想以非阻塞方式成功回收进程,可以定期执行waitpid()
,直到成功回收子进程的状态信息。示例程序如下所示(仅展示关键代码):
void test_waitpid()
{pid_t pid;pid = fork();if(pid > 0){pid_t retval;int status;printf("parent process is waiting...\n");while((retval = waitpid(-1, &status, WNOHANG)) == 0){sleep(2); //非阻塞等待子进程退出,如果未退出则睡眠一段时间}printf("parent process wait done, retval = %d status = %d\n", retval, status);}else if(0 == pid){int cnt = 0;while(cnt < 4){cnt++;printf("child process is running\n");sleep(2);}exit(0);}else{perror("create process failed\n");exit(-1);}
}
执行该段代码编译生成的可执行文件可得到如下结果:
父进程以一定的时间间隔非阻塞等待子进程退出,当子进程退出后成功回收其状态信息。
2、写在最后
Linux程序设计是一门很深的学问,由于时间关系,还有很多我想补充的内容都未能放在这上面。即将开启一段新的旅程了,日后有时间还会不断地去完善,祝好运!
Linux程序设计—多进程编程相关推荐
- linux下多进程编程简介
两年前的文章,拿过来充充门面. ------------------------ linux下多进程编程简介 ( 作者:mikespook | 发布日期:2002-12-8 | 浏览次数:272 ) ...
- Linux程序设计-3-Linux编程准备知识
Linux Programming Prerequisite 1. 编程原则 抽象和具体 库(API)的调用与选择:从技术角度,一般使用标准库,如果使用商业库,则会给对方平台带来一定的收益,但是对自己 ...
- linux编写多进程程序实验,实验7 编写多进程程序
实验七编写多进程程序 学生姓名:李亚军学号:6100412196 专业班级:卓越计科121班 1.实验目的 通过编写多进程程序,使读者熟练掌握fork().exec().wait()和waitpid( ...
- socket多进程编程
socket多进程编程 一.服务器并发访问的问题 服务器按处理方式可以分为迭代服务器和并发服务器两类.平常用C写的简单Socket客户端服务器通信,服务器每次只能处理一个客户的请求,它实现简单但效率很 ...
- linux线程并不真正并行,多核时代:并行程序设计探讨(3)——Windows和Linux对决(多进程多线程)...
并行程序设计探讨(3)--Windows和Linux对决(多进程多线程) 前面的博文经过分析总结,最后得出两种并行技术:多进程多线程.多机协作.对于多进程和多线程来说,最有代表性且最常见的的莫过于Wi ...
- linux 面包店 多进程,Linux下的多进程编程(一)
什么是一个进程?进程这个概念是针对系统而不是针对用户的,对用户来说,他面对的概念是程序.当用户敲入命令执行一个程序的时候,对系统而言,它将启动一个进程.但和程序不同的是,在这个进程中,系统可能需要再启 ...
- linux c多进程多线程,linux下的C\C++多进程多线程编程实例详解
linux下的C\C++多进程多线程编程实例详解 1.多进程编程 #include #include #include int main() { pid_t child_pid; /* 创建一个子进程 ...
- Linux多进程编程(2)
简介 IPC(Inter Process Communication,进程间通信)的方式总共有三种,分别是信号量.共享内存和消息队列,本文介绍前两种. 在Linux中,进程之间操作公共数据时,需要进行 ...
- Linux -- 多进程编程之 - 守护进程
内容概要 一.守护进程概述 二.守护进程创建 2.1.创建子进程,父进程退出 2.2.在子进程中创建新会话 2.2.1.进程组和会话期 2.2.2.setsid()函数说明 2.3.改变当前工作目录 ...
最新文章
- python初始化函数_当你学会了Python爬虫,网上的图片素材就免费了
- lumen php命令,php – 如何使用命令行手动运行laravel / lumen作业
- java中奇偶数的判断
- 十分钟完成的操作系统
- 也谈创业企业CEO该拿多少工资
- 最受欢迎Java数据库访问框架大比拼,你独爱哪一款?
- 利用Tushare合成期货主力连续数据
- Photoshop CC 2019魔棒工具的抠图
- 温度补偿计算公式_基于温度压力补偿计算的燃气表计量方法与流程
- beego golang bootstrap-table做月度考勤(打卡、签到)统计表
- 【Day4.3】大皇宫内蹭讲解
- HDU CCPC网络选拔赛 6441 Find Integer(数学)
- Fail to allocate bitmap
- php【websocket】
- Mac无法安装第三方软件
- html中实现页面跳转代码怎么写,用JavaScript怎么实现页面跳转?
- 《前端技巧》清理微信浏览网站的缓存,Cookie
- 十进制转十六进制 代码
- verilog中将fft转换成ifft
- android的多开器解析和检测实现