学习C高级(二十二)
多进程编程
什么是进程(process)
程序这个词含义很广泛,只要是能代表能完成某个功能的实体都可以称为程序,如:
- 用流程图表示的某个处理过程 ----- 流程图 ---- 不可执行
- 用高级语言语句编写的源码文件 ----- 源码文件 ---- 不可执行
- 编译好可执行文件 ------ 可执行文件 ----- 可执行
- 可执行文件加载到内存中去执行形成的进程 ---- 进程
进程:可执行文件加载到内存中形成的执行实体 或 程序在内存中执行的实体 或 正在执行的程序
- OS中任务的一种,意味着
a. 参与时间片轮转
b. 有五个状态 - 是操作系统分配资源的基本单位,意味着
a. 每个进程都有内存四区 — 进程的内存布局
b. 不同的进程除了代码区可以共享,其它三区互不干扰,各自为政 - 操作系统为了管理多个进程,给每一个进程一个唯一的身份标示,
这个身份标示被称为进程ID(pid)
函数名:getpid
函数原型:pid_t getpid();
函数功能:获取调用进程的pid
函数返回值:当前进程的pid
- 操作系统采用多结构形式管理批量进程,其中核心结构为树,树上每个节点就是一个进程。
a. 每个进程都有一个父进程,根节点可以认为是操作系统本身其pid为0,
第一个应用进程init(祖先进程)的pid为1,其它进程都是它的子孙
函数名:getppid
函数原型:pid_t getppid(); //parent process-id
函数功能:获取调用进程的父进程的pid
函数返回值:当前进程老爹的pid
b. 每个进程都可以有0个或多个子进程
c. 除了第一个应用进程,其它进程都是由其父进程创建的
d. 当一个进程的父进程先退出,该进程会成为孤儿进程,系统会将该进程的父进程指定为祖先进程:
1) 老版Linux为init进程(pid为1)
2) 新版Linux为systemd进程(init进程的儿子)
ps -ef
侧重显示父子进程
ps -aux
侧重显示资源占用率和进程状态
5. 前台进程和后台进程
前台进程 – 能使用标准输入设备的进程
一个控制台只能有一个
后台进程 – 不能使用标准输入设备的进程
kill -9 进程号
6. Linux操作系统内部使用一个结构体(struct task_struct)来描述每一个任务的所有属性数据,该结构体被称为进程控制块(PCB),
主要成员有:内存四区地址,pid,用来管理已打开文件用的数组等等
7. 每个进程为了管理已打开的文件,用一个数组(描述符数组)存放多个已打开文件的属性
所谓文件描述符就是描述符数组的下标
//操作系统内部数据类型,应用程序不可用,每次open产生一个
struct filetable {
int flags;//存放open函数的第二个参数
int loc;//位置指示器
int refnum;//引用计数
//其它成员.....
};
//操作系统内部数据类型,应用程序不可用
struct fddata{
int useflag;//0未被使用,1已被使用
struct filetable *pstAttr;
}
struct fddata fdarray[N];
两种情况的区别:
- dup — 数组下标为oldfd的元素拷贝到另一个未被使用的元素空间中
int dup(int oldfd)
两个描述符使用的是同一个struct filetable类型的元素,只是将struct filetable中的引用计数成员++
关闭一个描述符时,只是将对应引用计数减一,直到为0,才被销毁
使用的是同一个位置指示器 - 打开同一个文件两次
两个描述符使用的是各自的struct filetable类型元素
使用的是各自位置指示器
小结:进程是正在执行的程序,它是参与CPU时间片轮转的任务,
也是系统分配资源的基本单元
每个进程都有内存四区和管理已打开文件用的数组
进程的退出
-
main函数返回 ------ 项目中的推荐做法
-
exit ----- 优雅的中途退出 ---- 不推荐
可以配套使用atexit来注册清理函数void exit(int status)
功能:中途退出进程
参数:status:表示中途退出的情形(0表示正常,非0表示因为错误) -
abort ----- 粗暴的中途退出 ---- 禁止
系统维护任务中的全局变量
系统给每个任务(进程和线程)维护着一个全局变量errno(整型),用于记录最近一次的调用系统函数发生的错误号
包含#include <errno.h>后,代码可以直接使用errno这个变量名,这个变量的赋值是所有系统调用函数去做的(发送错误时),应用编程自己代码中不要对它赋值,但可以读出它的值
perror
void perror(const char *s)
功能:先打印s指向的字符串,再打印当前errno值对应错误描述
strerror
char *strerror(int errornum)
功能:返回指定错误对应的错误描述字符串(位于数据区、只读的)
参数:errornum:填errno的当前值
返回值:错误描述字符串所在空间的首地址
用perror函数打印出错信息
/*
用perror函数打印打开文件失败的信息
*/
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <errno.h>
#include <stdio.h>
#include <string.h>
int main(int argc,char *argv[]){
int fd = -1;
fd = open("sgdfhasgdh",O_RDONLY);//sgdfhasgdh这个文件不存在打开失败
if(fd < 0){
//打印出错信息的方式一
/*printf("errno=%d\n",errno);
perror("open failed");*/
//打印出错信息的方式二
printf("errno=%d,error string:%s\n",errno,strerror(errno));
return 1;
}
return 0;
}
创建子进程
pid_t fork()
功能:通过拷贝调用进程自身来创建新的子进程
返回值:
< 0 出错
在父进程返回子进程pid
在子进程返回为0
- 子进程获得父进程数据区、堆和栈的副本,而共享代码区
- 父进程的描述符数组拷贝给子进程的描述符数组
- 项目中fork后的用法:
1) 一个父进程希望复制自己,使父子进程执行大部分相同部分不同
或完全相同的代码逻辑,
网络编程中经常用来处理服务请求
2) 一个进程要执行一个不同的程序,即子进程执行exec
fork前:
- 只有一个进程 ---- 父进程
fork后
-
父进程
fork函数返回值为子进程的pid(因此>0) -
子进程
1) 子进程从父进程获取了fork前代码采用的数据结果(数据区、堆、栈)
2) 但fork返回值与父进程不一样 ---- 0
3) 描述符数组也是从父进程copy过来的可以认为,子进程在fork前代码产生的数据结果基础上执行fork后的代码
用fork函数创建子进程
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int gx = 10;
int main()
{
int lx = 100;
pid_t spid = 0;
spid = fork();//创建子进程
if(spid < 0){//子进程创建失败退出
perror("fork error");
return 1;
}
if(spid == 0){//子进程才执行的代码
lx += 10;
gx -= 5;
printf("son-process lx=%d,gx=%d\n",lx,gx);
printf("son-process id is %d\n",getpid());
}else{//父进程才执行的代码
printf("father-process lx=%d,gx=%d\n",lx,gx);
printf("father-process id is %d\n",getpid());
}
//父子进程都执行的代码
printf("My ID is %d,My Father-Process ID is %d\n",getpid(),getppid());
/*
sleep
unsigned int sleep(unsigned int seconds)
功能:让调用任务睡眠seconds秒
*/
sleep(1);//目的是一定程度上防止孤儿进程
return 0;
}
对子进程进行善后处理
僵尸进程:
当子进程代码已退出,而父进程继续执行,并且父进程没有对子进程进行善后处理,此时子进程处于僵死态,处于僵死态的进程被称为僵尸进程
处于僵死态的进程其实就是内存泄漏,因此要避免。
如何避免僵尸进程:
- 父进程中显式调用wait函数对子进程进行善后
wait
pid_t wait(int *wstatus)
waitpid
pid_t waitpid(pid_t pid,int *wstatus,int options)
功能:显式功能:如果调用进程没有子进程退出则等待有子进程退出
隐藏功能:
1. 一旦有子进程该函数将回收对应子进程占用的资源(善后)
2. 获取已退出子进程的退出码
参数:
wstatus:结果参数,其指向空间用于存放已退出子进程的退出码(其中包含子进程的main函数返回值)
返回值:
正常返回已退出子进程的pid
失败-1
备注1:
waitpid可以等待指定的子进程退出,指定子进程通过pid参数指定
waitpid可以变成非阻塞型,通过将options参数指定成WNOHANG
备注2:
获取子进程main函数返回值的方法:
WEXITSTATUS(wstatus)
- 让后代进程成为孤儿进程,从而让祖先进程对其善后
获取子进程的返回值
/*
获取子进程的返回值
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t spid = 0;
spid = fork();
if(spid < 0){
perror("fork failed");
return 1;
}
if(spid == 0){//子进程才执行的代码
printf("In son process\n");
return 123;//子进程main的返回值为123
}else{//父进程才执行的代码
int retval = 0;
wait(&retval);//其指向空间用于存放已退出子进程的退出码
//(其中包含子进程的main函数返回值)
printf("The son process return %d\n",WEXITSTATUS(retval));//从退出码中得到子进程的返回值
}
return 0;
}
创建进程扇
/*
创建一个进程扇,即一个父进程三个子进程,注意僵死态处理
*/
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
int main(){
pid_t pid = 0;
int i = 0;
for(i = 0;i < 3;i++){
pid = fork();//创建子进程
if(pid < 0){//创建子进程失败就不再创建
perror("fork error");
break;
}
if(pid == 0){//子进程才执行的代码
printf("My pid is %d,My ppid is %d\n",getpid(),getppid());
return 0;//注意,这里的return 目的是为了子进程不再执行后续代码
}
}
//这里只有父进程执行,因为子进程已经不再执行后续代码
for(i = 0;i < 3;i++){
wait(NULL);//等待子进程退出,对其进行善后处理以避免子进程成为僵尸进程
}
return 0;
}
创建进程链
/*
创建一个进程链,即一个父进程一个子进程一个孙子进程,注意僵死态处理
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t spid = 0;
pid_t gpid = 0;
spid = fork();//创建子进程
if(spid < 0){//创建子进程失败退出
perror("fork son-process error");
return 1;
}
if(spid == 0){//子进程才执行的代码
gpid = fork();//创建子进程的子进程,即第一个进程的孙子进程
if(gpid < 0){//创建孙子进程失败退出
perror("fork grandson-process error");
return 1;
}
if(gpid == 0){//孙子进程才执行的代码
printf("In Grandson-Process,pid is %d,ppid is %d\n",getpid(),getppid());
return 0;//目的是使孙子进程的后续代码不再执行
}else{//子进程才执行的代码
printf("In Son-Process,pid is %d,ppid is %d\n",getpid(),getppid());
wait(NULL);//对孙子进程做善后处理
return 0;//目的是使子进程的后续代码不再执行
}
}
else{//因为子进程和孙子进程都不再执行后续代码,这只有父进程才执行的代码
printf("In Father-Process,pid is %d,ppid is %d\n",getpid(),getppid());
wait(NULL);//对子进程做善后处理
}
return 0;
}
替换当前进程
exec系列一共有六个函数
int execl(const char *path,const char *arg,…,NULL)
int execlp(const char *name,const char *arg,…,NULL)
功能:
替换当前进程为指定的可执行文件对应的程序
参数:
path:指向空间存放着一个字符串,
该字符串的内容为带路径的可执行文件名,
如果无路径默认为当前目录下的可执行文件
name:指向空间存放着一个字符串,
该字符串的内容为不带路径的可执行文件名,
函数会在PATH环境变量指定目录下找可执行文件
arg:给新程序main函数argv[0]的地址,该地址空间中存放着一个字符串
…:给新程序main函数argv[1]、argv[2]、…的地址,
这些地址空间中都存放着一个字符串
NULL:给新程序main函数argv[argc]的地址
返回值:
正常不返回
错误返回-1
子进程调用替换进程函数的处理过程:
- 数据区按新程序需要重新组织
- 栈区清空,从头开始执行新程序
- 堆区,替换前的动态空间统统被释放
- 代码区不再与父进程共享,而是重新生成自己独立代码区
- 描述符数组里已打开的文件,
有的会被关闭(open时有O_CLOEXEC标记)
有的不会被关闭(open时没有O_CLOEXEC标记)
建议,调用exec前自行关闭
system函数的设计
//system函数的实现
int system(char *command){
pid_t pid = 0;
pid = fork();
if(pid < 0){
return -1;
}
if(pid == 0){
execlp(commmand,command,NULL);
}else{
wait(NULL);
}
return 0;
}
用execlp函数实现简易命令行程序
/*
实现一个简易的命令行界面程序,
父进程接收用户的输入,然后创建子进程,再调execlp执行命令
*/
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int MyGetString(char arr[],int size);
int main(){
char buf[64] = "";
pid_t pid = 0;
while(1){
/*接收用户输入的命令*/
printf("MyCommand$");
MyGetString(buf,64);
if(strcmp(buf,"quit") == 0){
break;
}
/*fork子进程*/
pid = fork();
if(pid < 0){
perror("fork failed");
continue;//创建子进程失败时不用return退出的原因是因为用户没输入quit
}
if(pid == 0){
execlp(buf,buf,NULL);//替换子进程
printf("Command not found\n");
}else{
wait(NULL);//善后处理子进程
}
}
return 0;
}
//函数功能:接收size个字符的字符串
int MyGetString(char arr[],int size){
int len = 0;
fgets(arr,size,stdin);
len = strlen(arr);
if(arr[len-1] == '\n'){//字符串以'\0'结尾
arr[len-1] = '\0';
}else{
while(getchar() != '\n')
{//什么都不运行的目的是清空缓冲器
}
}
return 0;
}
用execl函数运行其他可执行文件
execl的使用:现在有execl.c和test.c两个文件,test.c编译后的可执行文件命名为test,execl.c编译后默认可执行文件名为a.out,现在通过a.out运行test
/*
execl.c
*/
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdio.h>
int main(){
pid_t pid = 0;
pid = fork();
if(pid < 0){
perror("fork failed");
return 1;
}
if(pid == 0){
int ret = 0;
ret = execl("./test","./test","def1","def2",NULL);
}else{
wait(NULL);
}
return 0;
}
/*
test.c
*/
#include <unistd.h>
#include <stdio.h>
int main(int argc,char *argv[]){
int i = 0;
printf("argc=%d\n",argc);
while(argv[i] != NULL){
printf("The argv[%d] content is %s\n",i,argv[i]);
i++;
}
return 0;
}