Linux编程基础:进程管理2

一、进程管理

1.exec函数族

使用fork()函数创建的子进程,其中包含的程序代码完全相同,只能根据fork()函数的返回值,执行不同的代码分支。
由exec函数族中的函数,则可以根据指定的文件名或路径,找到可执行文件。

  • fork:子进程复制父进程的堆栈段和数据段,子进程一旦开始运行,它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再影响
  • exec:一个进程调用exec类函数,它本身就"死亡"了,系统把代码段替换成新的程序代码,废弃原有数据段和堆栈段,并为新程序分配新数据段与堆栈段

exec函数族中包含6个函数,分别为:

在这里插入代码#include <unistd.h>
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);
int execv(const char *path, char * const argv[]);
int execvp(const char *file, char * const argv[]);
int execve(const char *path, char * const argv[], char * const envp[]);
参数说明

(1)当参数是path,传入的为路径名;当参数是file,传入的可执行文件名;

(2)可以将exec函数族分为execl和execv两类:

  • execl类:函数将以列举的形式传入参数,由于参数列表的长度不定,所以要用哨兵NULL表示列举结束;
  • execv类:函数将以参数向量表传递参数,char * argv[]的形式传递文件执行时使用的参数,数组中最后一个参数为NULL;
    (3)如果没有参数char * const envp[],则采用默认环境变量;如果有,则用传入的参数替换默认环境变量;

在 Linux 系统中,环境变量是用来定义系统运行环境的一些参数,比如每个用户不同的家目录(HOME)、邮件存放位置(MAIL)等
[root@localhost ~]# env
ORBIT_SOCKETDIR=/tmp/orbit-root
HOSTNAME=livecd.centos
GIO_LAUNCHED_DESKTOP_FILE_PID=2065
TERM=xterm
SHELL=/bin/bash

提醒一下,有些参数是path有些参数又是file两个有一点细微的区别。当参数是path时,传入的为命令的路径名;当参数是file,传入的可执行文件名。

至于函数几个函数到底该怎么使用,看看后面的案例就会明白怎么使用、有什么作用了。

案例
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
    pid_t tempPid;
    tempPid=fork();
    if(tempPid == -1){   
        perror("fork error");
        exit(1);
    } else if(tempPid > 0) {   
        printf("parent process:pid=%d\n", getpid());
    } else {   
        printf("child process:pid=%d\n", getpid());
        //execl("/bin/ls","-a","-l","./",NULL);	//①
        //execlp("ls","-a","-l","./",NULL);	//②
        char *arg[]={"-a","-l","./", NULL};	//③
        execvp("ls", arg);
        perror("error exec\n");
        printf("child process:pid=%d\n", getpid());
    } //of if  
    return 0;
} //of main

在这里插入图片描述

以上即为大概格式,可以用ls命令查看文件夹中的文件、以路径查看文件等等。除了ls还可以用其它的命令,只要时bash能执行的在exec家族中也能。

2.exit函数(进程退出)

#include <stdlib.h>
void exit(int status);
参数说明

(1)status:表示进程的退出状态,0表示正常退出,非0表示异常退出,一般用-1或1表示;
(2)为了可读性,标准C定义了两个宏:EXIT_SUCCESS和EXIT_FAILURE

_exit()函数
Linux系统中有一个与exit()函数非常相似的函数:_exit()

#include <unistd.h>
void _exit(int status);

区别:

  • _exit:系统会无条件停止操作,终止进程并清除进程所用内存空间及进程在内核中的各种数据结构;
  • exit:对_exit进行了包装,在调用_exit()之前先检查文件的打开情况,将缓冲区中的内容写回文件。相对来说exit比_exit更为安全

一般的,当遇到一个函数是以下划线开头,那么这个函数会比没有下划线的函数更贴近于底层。例如:exit() 与 _exit() 相比功能一样,但增加了退出时的安全性条件的判断。

特殊进程

  • 孤儿进程:父进程负责回收子进程,如果父进程在子进程退出之前退出,子进程就会变成孤儿进程,此时init进程将代替父进程完成子进程的回收工作;
  • 僵尸进程:调用exit函数后,该进程不会马上消失,而是留下一个称为僵尸进程的数据结构。它几乎放弃进程退出前占用的所有内存,既没有可执行代码也不能被调度,只是在进程列表中保留一个位置,记载进程的退出状态等信息供父进程回收。若父进程没有回收子进程的代码,子进程将会一直处于僵尸态。

特殊进程的危害

  • 僵尸进程不能再次被运行,会占用一定的内存空间,并占据进程编号,当僵尸进程较多时,将会消耗系统内存,新进程可能因内存不足或无法获取pid而无法创建;
  • 父进程通过wait()和waitpid()函数可以有效防止僵尸进程的产生,对于已存在的僵尸进程,则可通过杀死其父进程的方法解决;
  • 当僵尸进程的父进程被终止后,僵尸进程将作为孤儿进程被init进程接收,init进程会不断调用wait()函数获取子进程状态,对已处于僵尸态的进程进行处理;
  • 孤儿进程永远不会成为僵尸进程。

二、进程同步

在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。不同进程存在着相互制约的关系。

1.wait函数

#include <sys/wait.h>
pid_t wait(int *status);
功能
  • 挂起进程,进程进入阻塞状态,直到子进程变为僵尸态,如果捕获到子进程的退出信息就会转为运行态,然后回收子进程资源并返回;若没有变为僵尸态的子进程,wait函数就会让进程一直阻塞。若当前进程有多个子进程,只要捕获到一个变为僵尸态的子进程,wait函数就会恢复执行态。
参数说明
  • 参数status是一个int *类型的指针,用来保存子进程退出时的状态信息。通常情况下该参数设为NULL,表示不关心进程时如何终止的。
返回值说明
  • 成功:返回子进程的进程id;
  • 失败:返回-1,errno被设置为ECHILD。
案例

【案例 1】若子进p1是其父进程p的先决进程,基于wait函数使得进程同步。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main(){
	pid_t tempPid, tempW;
	tempPid = fork();
	if(tempPid == -1){
		perror("fork error");
		exit(1);
	}else if(tempPid == 0){//child
		sleep(3);
		printf("Child process, pid = %d, ppid = %d\n", getpid(), getppid());
	}else{//parent 
		tempW = wait(NULL);
		printf("Catched a child process, tempW = %d, ppid = %d\n", tempW, getppid());
	}//of if
	printf(">>>>>>>>finish<<<<<<<<\n");
	return 0;
}//of main

把父进程挂起就可以避免子进程变成孤儿进程,因为必须得子进程结束父进程的挂起才会结束。


若wait函数的参数不为空,可以获取子进程的退出状态,退出状态存放在参数status的低八位中。Linux定义了一组判断进程退出状态的宏函数,其中最基础的两个如下:

#include <sys/wait.h>
int WIFEXITED(int status);//判断子进程是否正常退出,若是,返回非0值,否则返回0
int WEXITSTATUS(int status);//和WIFEXITED配合使用,WIFEXITED返回非0值,则使用该宏提取子进程的返回值。

【案例 2】使用wait同步进程,并使用宏获取子进程的返回值。

test_wait2.c
#include <stdio.h>
#include <sys/wait.h>
#include <stdlib.h>
int main(){
    int tempStatus;
    pid_t tempPid, tempW;
    tempPid = fork();
    if(tempPid == -1){
        perror("fork error");
        exit(1);
    } else if(tempPid == 0){//子
        sleep(3);
        printf("Child process: pid=%d\n",getpid());
        exit(5);
 	}  else{//父
        tempW = wait(&tempStatus);
        if(WIFEXITED(tempStatus)){
            printf("Child process pid=%d exit normally.\n", tempW );
            printf("Return Code:%d\n",WEXITSTATUS(tempStatus));
        } else {
            printf("Child process pid=%d exit abnormally.\n", tempW);
        }//of if
    }//of if
    return 0;
}//of main

2.waitpid函数

wait虽然可用,但是也有很明显的缺点:如果存在多个子进程,那么只要其中一个子进程结束,那么父进程就解开了挂起状态,则后面的子进程都会成为孤儿进程。为了解决这个问题我们引用升级版wait函数waitpid()。

#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
功能
  • 可以应对 wait函数面临的缺点。可以等待指定的子进程,也可以在父进程不阻塞的情况下获取子进程的状态。
参数说明
  • pid:一般是进程的pid,也有可能是其他取值。进一步说明如下:
    – pid > 0:等待子进程(编号为pid)退出,若退出,函数返回;若未结束,则一直等待;
    – pid = 0:等待同一进程组的所有子进程退出,若某子进程加入了其他进程组,则waitpid不再关心它的状态;
    – pid = -1:waitpid函数退化为wait函数,阻塞等待并回收一个子进程;
    – pid < -1:等待指定进程组中的任何子进程,进程组的id等于pid的绝对值。
  • options: 提供控制选项,可以是一个常量,也可以是|连接的两个常量,选项如下:
    – WNOHANG:如果子进程没有终止,waitpid不会阻塞父进程,会立即返回;
    – WUNTRACED:如果子进程暂停执行,waitpid立即返回;
    – 0:不使用选项。
返回值说明
  • 成功:返回捕捉到的子进程id;
  • 0:options = WNOHANG, waitpid发现没有已退出的子进程可回收;
  • 1:出错,errno被设置。
案例

【案例 3】父进程等待进程组中指定子进程,该进程不退出,则父进程一直阻塞。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
int main(){
	pid_t tempPid, tempP, tempW;
	tempPid= fork();							//创建第一个子进程
	if (tempPid == -1){							
		perror("fork1 error");
		exit(1);
	} else if (tempPid == 0){						//子进程沉睡
		sleep(5);
		printf("First child process:pid=%d\n", getpid());
	} else {						//父进程继续创建进程
		int i;
		tempP = tempPid;
		for (i = 0; i < 3; i++){					//由父进程创建3个子进程
			if ((tempPid = fork()) == 0){
				break;
			}//of if
		}//of for i
		if (tempPid == -1){						//出错
			perror("fork error");
			exit(2);
		} else if (tempPid == 0){					//子进程
			printf("Child process:pid=%d\n", getpid());
			exit(0);
		} else {					//父进程
			tempW = waitpid(tempP, NULL, 0);			//等待第一个子进程执行
			if (tempW == tempP){
				printf("Catch a child Process: pid=%d\n", tempW);
			}else{
				printf("waitpid error\n");
			}//of if
		}//of if
	}//of if
	return 0;
}//of main

【案例 4】基于waitpid函数不断获取子进程的状态。

#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <unistd.h>
int main() {
	pid_t tempPid, tempW;
	tempPid = fork();
	if (tempPid == -1){
		perror("fork error");
		exit(1);
	} else if (tempPid == 0){
		sleep(3);
		printf("Child process:pid=%d\n", getpid());
		exit(0);
	} else {
		do{
			tempW = waitpid(tempPid, NULL, WNOHANG);
			if (tempW == 0){
				printf("No child exited\n");
				sleep(1);
			}//of if
		} while (tempW == 0);
		if (tempW == tempPid){
			printf("Catch a Child process:tempW=%d\n", tempW);
		}else{
			printf("waitpid error\n");
		}//of if
	}//of if
	return 0;
}//of main

三、 进程间通讯

通信故名思意就是进程与进程之间的信息交互

1.管道

我们通过一个缓冲区把信息从一个进程传到另一个进程,这个缓冲区我们把它称为管道。管道其实质是由内核管理的一个缓冲区,形象地认为管道的两端连接着两个进程:一个进程进行信息输出,将数据写入管道;另一个进程进行信息输入,从管道中读取信息。

在这里插入图片描述
写和读不能同时进行,也就是管道只能读或者写,如果两个进程同时对管道进行操作,则两个进程中必须有一个停止下来让另一个进行操作。

管道分为

  • 匿名管道:只能用于有亲缘关系的进程间通信,进程退出后管道会被销毁。
  • 命名管道:命名管道与进程的联系较弱,相当于一个读写内存的接口,进程退出后,命名管道依然存在。
1)匿名管道

匿名管道的使用流程如下:
①在进程中创建匿名管道,pipe函数;
②关闭进程中不使用的管道端口,close函数;
③在待通信的进程中分别对管道的读、写端口进行操作,read/write函数;
④关闭管道,close函数。

A.pipe函数
#include <unistd.h>
int pipe(int pipefd[2]);

功能

  • 创建匿名管道

参数说明

  • pipefd:传入参数,一个文件描述符数组;Linux将管道抽象为一个特殊文件。

返回值说明

  • 成功:返回0.
  • 不成功:返回-1。

【案例1】使用pipe()实现父子进程间通信,父进程作为读端,子进程作为写端。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(){
    int tempFd[2];//定义文件描述符数组
    int tempRet=pipe(tempFd);//创建管道
    if(tempRet == -1){   
        perror("pipe");
        exit(1);
    }   
    pid_t tempPid=fork();
    if(tempPid > 0){//父进程—读
        close(tempFd[1]);//关闭写端
        char tempBuf[64]={0};
        tempRet = read(tempFd[0], tempBuf, sizeof(tempBuf));//读数据
        close(tempFd[0]);
        write(STDOUT_FILENO, tempBuf, tempRet);//将读到的数据写到标准输出
        wait(NULL);
    } else if(tempPid == 0){//子进程—写
        close(tempFd[0]);//关闭读端
        char *tempStr="hello,pipe\n";
        write(tempFd[1], tempStr, strlen(tempStr)+1);//写数据
        close(tempFd[1]);
   }//of if
   return 0;
}//of main

分析如下:

pipe()创建管道后读端的文件描述符为fd[0],写端的文件描述符为fd[1];
调用fork后父子进程共享文件描述符,文件描述符与管道的关系如图所示:

在这里插入图片描述

父进程进行读操作,子进程进行写操作;使用close()函数关闭父进程的写端与子进程的读端。

在这里插入图片描述

在这里插入图片描述

B.dup2函数
#include <unistd.h>
int dup2(int oldfd, int newfd);

功能

  • 是将参数oldfd的文件描述符复制给newfd

返回值说明

  • 若函数调用成功则返回newfd
  • 否则返回-1,并设置errno。

【案例2】使用管道实现兄弟进程间通信,兄弟进程实现命令“ls | wc –l”的功能。

(在实现本案例时会用到重定向函数dup2:)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
    int tempFd[2];
    int tempRet = pipe(tempFd);
    if(tempRet == -1){
        perror("pipe err");
        exit(1);
    }//of if   
    int i;
    pid_t tempPid, tempWpid;
    for(i=0; i<2; i++){//2个子
        if((tempPid = fork()) == 0){
            break;
        }//of if
    }//of if
	if(2 == i){//父进程,回收子进程
        close(tempFd[0]); //关闭读
        close(tempFd[1]); //关闭写
        tempWpid = wait(NULL);
        printf("wait child 1 success,pid=%d\n", tempWpid );
        tempPid = wait(NULL);
        printf("wait child 2 success,pid=%d\n", tempPid);
    } else if(0 == i){//子进程1—写
        close(tempFd[0]);
        dup2(tempFd[1], STDOUT_FILENO);//定向到标准输出
        execlp("ls", "ls", NULL);
    } else if(1 == i){//子进程2—读
        close(tempFd[1]);
        dup2(tempFd[0], STDIN_FILENO);
        execlp("wc", "wc", "-l", NULL);
    }//of if
    return 0;
}//of main

兄弟进程间通信,进程文件描述符与管道的关系如图所示:

在这里插入图片描述

实线所示的箭头为编程中需要保留的文件描述符

C.popen/pclose函数

Linux标准I/O库两个函数popen()和pclose(),可完成管道通信的流程。

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

功能

popen函数的功能是:

  • 调用pipe()函数创建管道;
  • 调用fork()函数创建子进程;
  • 之后在子进程中通过execve()函数调用shell命令执行相应功能。

pclose函数的功能:

  • 关闭popen()打开的I/O流;
  • 通过调用wait()函数等待子进程命令执行结束;
  • 返回shell的终止状态,防止产生僵尸进程。

参数说明

popen函数的参数:

  • command:命令;
  • type:指定命令类型(输入w/输出r);

pclose函数的返回值:

  • 成功:0;
  • 不成功:-1。

【案例3】使用popen与pclose函数实现管道通信。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
    FILE *tempRfp,*tempWfp;
    char temBuf[100];
    tempRfp = popen("ls","r");		//读取命令执行结果
    tempWfp = popen("wc -l","w");	//将管道中的数据传递给进程
    while(fgets(temBuf, sizeof(temBuf), tempRfp)!=NULL){
    	fputs(temBuf, tempWfp);
    }//of while   
    pclose(tempRfp);
    pclose(tempWfp);
    return 0;
}//of main
2)命名管道
A.mkfifo函数
#include <sys/type.h>
#include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

功能

  • 创建命名管道(FIFO文件),命名管道与系统中的一个路径名关联,以文件的形式存在于文件系统中,通过FIFO的路径名访问FIFO文件,实现进程间通信。

参数说明

  • pathname:管道文件的路径名,通过FIFO路径名访问FIFO文件;
  • mode:指定FIFO的权限;

返回值说明

  • 成功:0;
  • 不成功:-1,并设置errno。

【案例4】使用FIFO实现没有亲缘关系进程间的通信。没有亲缘关系的进程间通信,需要两段程序来实现:

  • fifo_write.c实现FIFO的写操作;
  • fifo_read.c实现FIFO的读操作。
fifo_write.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int paraArgc,char *paraArgv[]){
    if(paraArgc < 2){							//判断是否传入文件名
        printf("./a.out fifoname\n");
        exit(1);
    }//of if
    int tempRet = access(paraArgv[1], F_OK);	//判断fifo文件是否存在
    if(tempRet == -1){							//若fifo不存在就创建fifo 
        int tempFIFO = mkfifo(argv[1], 0664);
        if(tempFIFO == -1){						//判断文件是否创建成功
            perror("mkfifo");
            exit(1);
        } else{
            printf("fifo creat success!\n");
        }//of if
    }//of if
   int tempFd = open(argv[1], O_WRONLY);		//读写方式打开
   while(1){									//循环写入数据
        char *tempStrp="hello,world!";
        write(tempFd, tempStrp, strlen(tempStrp)+1);
        sleep(1);
    }//of while
    close(tempFd);
    return 0;
}//of main

fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int paraArgc,char *paraArgv[]){
    if(paraArgc < 2){						//判断是否传入文件名
        printf("./a.out fifoname\n");
        exit(1);
    }//of if
    int tempRet = access(argv[1], F_OK);	//判断fifo文件是否存在
    if(tempRet == -1){						//若fifo不存在就创建fifo 
        int tempFIFO = mkfifo(argv[1], 0664);
        if(tempFIFO == -1){					//判断文件是否创建成功
            perror("mkfifo");
            exit(1);
        } else{
            printf("fifo creat success!\n");
        }//of if
    }//of if
    int tempFd = open(argv[1], O_RDONLY);	//只读方式打开
    if(tempFd == -1){
        perror("open");
        exit(1);
    }//of if
    while(1){								//不断读取fifo中的数据并打印
        char temBuf[1024]={0};
        read(tempFd, temBuf, sizeof(temBuf));
        printf("buf=%s\n", temBuf);
    }//of while
    close(tempFd);							//关闭文件
    return 0;
}//of main

参考:Linux编程基础 5.1:进程间通讯-1_HenrySmale的博客

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值