学习C高级(二十二)

多进程编程

什么是进程(process)
  程序这个词含义很广泛,只要是能代表能完成某个功能的实体都可以称为程序,如:

  1. 用流程图表示的某个处理过程 ----- 流程图 ---- 不可执行
  2. 用高级语言语句编写的源码文件 ----- 源码文件 ---- 不可执行
  3. 编译好可执行文件 ------ 可执行文件 ----- 可执行
  4. 可执行文件加载到内存中去执行形成的进程 ---- 进程

进程:可执行文件加载到内存中形成的执行实体 或 程序在内存中执行的实体 或 正在执行的程序

  1. OS中任务的一种,意味着
    a. 参与时间片轮转
    b. 有五个状态
  2. 是操作系统分配资源的基本单位,意味着
    a. 每个进程都有内存四区 — 进程的内存布局
    b. 不同的进程除了代码区可以共享,其它三区互不干扰,各自为政
  3. 操作系统为了管理多个进程,给每一个进程一个唯一的身份标示,
    这个身份标示被称为进程ID(pid)

  函数名:getpid
  函数原型:pid_t getpid();
  函数功能:获取调用进程的pid
  函数返回值:当前进程的pid

  1. 操作系统采用多结构形式管理批量进程,其中核心结构为树,树上每个节点就是一个进程。
      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];

两种情况的区别:

  1. dup — 数组下标为oldfd的元素拷贝到另一个未被使用的元素空间中
    int dup(int oldfd)
    两个描述符使用的是同一个struct filetable类型的元素,只是将struct filetable中的引用计数成员++
    关闭一个描述符时,只是将对应引用计数减一,直到为0,才被销毁
    使用的是同一个位置指示器
  2. 打开同一个文件两次
    两个描述符使用的是各自的struct filetable类型元素
    使用的是各自位置指示器

小结:进程是正在执行的程序,它是参与CPU时间片轮转的任务,
也是系统分配资源的基本单元

每个进程都有内存四区和管理已打开文件用的数组

进程的退出

  1. main函数返回 ------ 项目中的推荐做法

  2. exit ----- 优雅的中途退出 ---- 不推荐
    可以配套使用atexit来注册清理函数

    void exit(int status)
    功能:中途退出进程
    参数:status:表示中途退出的情形(0表示正常,非0表示因为错误)

  3. 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

  1. 子进程获得父进程数据区、堆和栈的副本,而共享代码区
  2. 父进程的描述符数组拷贝给子进程的描述符数组
  3. 项目中fork后的用法:
    1) 一个父进程希望复制自己,使父子进程执行大部分相同部分不同
    或完全相同的代码逻辑,
    网络编程中经常用来处理服务请求
    2) 一个进程要执行一个不同的程序,即子进程执行exec

fork前:

  1. 只有一个进程 ---- 父进程

fork后

  1. 父进程
    fork函数返回值为子进程的pid(因此>0)

  2. 子进程
    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;
}

在这里插入图片描述

对子进程进行善后处理

僵尸进程:
  当子进程代码已退出,而父进程继续执行,并且父进程没有对子进程进行善后处理,此时子进程处于僵死态,处于僵死态的进程被称为僵尸进程
  处于僵死态的进程其实就是内存泄漏,因此要避免。
如何避免僵尸进程:

  1. 父进程中显式调用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)

  1. 让后代进程成为孤儿进程,从而让祖先进程对其善后

获取子进程的返回值

/*
获取子进程的返回值
*/
#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

子进程调用替换进程函数的处理过程:

  1. 数据区按新程序需要重新组织
  2. 栈区清空,从头开始执行新程序
  3. 堆区,替换前的动态空间统统被释放
  4. 代码区不再与父进程共享,而是重新生成自己独立代码区
  5. 描述符数组里已打开的文件,
      有的会被关闭(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;
}

在这里插入图片描述


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值