进程管理3.2
3.2.1exec()函数族
使用fork()函数创建的子进程中包含的程序代码完全相同,只是能根据fork()函数的返回值执行不同的代码分支。当每个分支的内容较多时,代码自身便较为庞大;另外,若要执行的分支与程序其他内容并不相干,对子进程来说,对除与之对应的分支外,其他的内容都是无意义的。
由exec函数族中的函数,则可以根据指定的文件名或路径,找到可执行文件。
注意:1)调用exec函数族时,不创建新进程,因此进程的pid不会改变;
2)exec只是用新程序中的数据替换了进程中的代码段、数据段、堆栈中的数据;
3)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.当以execl为前缀,则函数会使用默认的环境变量;若以execv为前缀,表示参数列表中对应有参数char * const envp[ ] --该参数用于接收用户传入的环境变量,则exec函数会使用接收到的环境变量代替默认的环境变量。
测试案例:
#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","test_exec.c",NULL); //①
//execlp("ls","-a","-l","test_exec.c",NULL); //②
char *arg[]={"-a","-l","test_exec.c", NULL}; //③
execvp("ls", arg);
perror("error exec\n");
printf("child process:pid=%d\n", getpid());
} //of if
return 0;
} //of main
运行截图:
这里只展示execvp()函数对ls命令进行调用的结果,代码中的三个函数最终输出结果相同;
3.2.2进程退出
Linux系统中进程的退出通过exit()函数实现。exit()函数存在于系统函数库stdlib.h中,其声明如下:
#include <stdlib.h>
void exit(int status);
参数说明:
1)status:表示进程的退出状态,0表示正常退出,非0表示异常退出,一般用-1或1表示;
2)为了可读性,标准C定义了两个宏:EXIT_SUCCESS和EXIT_FAILURE
Linux系统中有一个与exit()函数非常相似的函数:_exit(),其声明如下:
#include <unistd.h>
void _exit(int status);
区别:
_exit()函数:系统会无条件停止操作,终止进程并清除进程所用内存空间以及进程在内核中的各种数据结构;
exit()函数:对_exit进行了包装,在调用_exit()之前先检查文件的打开情况,将缓冲区中的内容写回文件。相对来说exit比_exit更为安全
3.3进程同步
Linux系统中提供了wait()函数和waitpid()函数来获取当前进程的状态,实现进程同步。
3.3.1 wait()函数
wait()函数存在于系统库函数sys/wait.h库中,其声明如下:
#include <sys/wait.h>
pid_t wait(int *status);
功能:调用wait()函数的进程会被挂起,进入阻塞状态,直到子进程变为僵尸态,wait()函数捕获到该子进程的退出信息时才会转为运行态,收回子进程资源并返回;若没有变为僵尸态的子进程,wait函数就会让进程一直阻塞。若当前进程有多个子进程,只要捕获到一个变为僵尸态的子进程,wait函数就会恢复执行态。
参数说明:参数status是一个int * 类型的指针,用来保存子进程退出时的状态信息。但是通常我们只想消灭僵尸进程,不在意程序如何终止,因此一般将该参数设置为NULL。
返回值说明:
成功:返回子进程的进程id;
失败:返回-1,errno被设置为ECHILD。
测试案例:
#inlcude <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.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, pid = %d, ppid = %d\n", tempW, getppid());
}//of if
printf("......finish......");
return 0;
}//of main
运行截图:
以上结果在执行程序后三秒后输出,因为利用sleep()函数将子进程沉睡三秒才执行;因为 父进程中执行的操作只是回收子进程,所以父进程在子进程结束后立刻输出。
注意:wait()函数中的参数可以不为空。若wait函数的参数不为空,可以获取子进程的退出状态,退出状态存放在参数status的低八位中。
Linux定义了一组判断进程退出状态的宏函数,其中最基础的两个是:
#include <sys/wait.h>
int WIFEXITED(int status);//判断子进程是否正常退出,若是,返回非0值,否则返回0
int WEXITSTATUS(int status);//和WIFEXITED配合使用,WIFEXITED返回非0值,则使用该宏提取子进程的返回值。
测试案例:使用wait同步进程,并使用宏获取子进程的返回值。
#include <unistd.h>
#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);argv
} 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
运行截图:
案例代码中定义了一个整型变量status,该变量在wait()函数中获取该子进程的退出码。之后通过宏WIFEXITED判断返回码是否为0,当不为0时,使用宏WEXITSTATUS将返回码转换为一个整型数据。
3.3.2 waitpid()函数
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
功能:
wait函数的缺点:当前进程有很多个子进程,wait函数无法保证所有子进程在父进程之前执行。
waitpid函数:可以应对 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被设置。
测试案例:父进程等待进程组中指定子进程,该进程不退出,则父进程一直阻塞。
#include <unistd.h>
#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
运行截图:
测试案例:基于waitpid函数不断获取子进程的状态。
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/wait.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:pid=%d\n", w);
}else{
printf("waitpid error\n");
}//of if
}//of if
return 0;
}//of main
运行截图:
3.3.3特殊进程的危害
僵尸进程不能再次被运行,但是会占用一定的内存空间:当系统中僵尸进程的数量很多时,不光会占用系统内存,还会占用进程id;若僵尸进程一直存在,新的进程可能会因为内存不足或一直无法获取pid而无法被创建。
因此,应尽量避免僵尸进程的产生,调用wait()和waitpid()可以有效避免僵尸进程产生。
父进程通过wait()和waitpid()函数可以有效防止僵尸进程的产生,对于已存在的僵尸进程,则可通过杀死其父进程的方法解决。
当僵尸进程的父进程被终止后,僵尸进程将作为孤儿进程被init进程接收,init进程会不断调用wait()函数获取子进程状态,对已处于僵尸态的进程进行处理。
注意:孤儿进程永远不会成为僵尸进程
4.1 进程间通信
4.1.1 管道
管道是一种最基本的进程通信机制,其实质是由内核管理的一个缓冲区。可以形象的认为管道的两端连接着两个需要进行通信的进程,一个进程进行信息输出,将数据写入管道;另一个进程进行信息输入,从管道中读取信息。
4.1.2 匿名管道
匿名管道的使用流程如下:
①在进程中创建匿名管道,pipe函数;
②关闭进程中不使用的管道端口,close函数;
③在待通信的进程中分别对管道的读、写端口进行操作,read/write函数;
④关闭管道,close函数。
4.1.3 pipe()函数
该函数声明如下:
#include <unistd.h>
int pipe(int pipefd[2]);
功能:
创建匿名管道
参数说明:
pipe()的参数pipfd实质上是一个文件描述符数组,当在程序中使用pipe()创建管道时,程序可以通过传参的方式获取两个文件描述符,分别交给需要通信的两个进程。
返回值说明:
成功:返回0
不成功:返回-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
运行截图:
由执行结果可知:父进程在管道中写入字符串"hello,pipe",子进程从管道中将该字符串读取并打印到了终端。使用close()函数关闭父进程的写端与子进程的读端。
4.1.3 dup2()函数
#include <unistd.h>
int dup2(int oldfd, int newfd);
功能:将参数oldfd的文件描述符传递给newfd
返回值说明:
成功:返回newfd
失败:返回-1,并设置error
测试案例:
#include <sys/wait.h>
#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
运行截图:
注意:匿名管道不可共用,因此父进程中管道的文件描述符必须要关闭,否则父进程中的读端会使进程阻塞。
其中,"4"为兄弟进程对ls | wc -l命令的实现,在终端中输入该命令后,发现输出结果相同,可知案例成功!
4.1.4 popen()/pclose()函数
管道通信的一般流程是:在一个进程中创建管道,通过fork()函数创建子进程,关闭管道多余端口,使父子进程与管道形成单向通道,进行数据传输。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函数的参数:
stream:I/O流
返回值说明:
popen函数的返回值:
成功:管道文件的描述符
不成功:返回-1
pclose函数的返回值:
成功:返回0
不成功:返回-1
测试案例:使用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
运行截图:
由输出结果可知,输出"4"为进程对ls | wc -l命令的实现,在终端输入命令后,输出结果与其相同,证明案例成功;同时也说明,利用popen()和pclose()函数实现的代码更加简洁。
4.1.5 命名管道
#include <sys/type.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
功能:
创建命名管道(FIFO文件),命名管道与系统中的一个路径名关联,以文件的形式存在于文件系统中,通过FIFO的路径名访问FIFO文件,实现进程间通信。
参数说明:
pathname:表示管道文件路径名
mode:用于指定FIFO的权限
返回值说明:
成功:0
不成功:-1,并设置errno
测试案例:使用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(paraArgc[1], 0664);
if(tempFIFO == -1){ //判断文件是否创建成功
perror("mkfifo");
exit(1);
} else{
printf("fifo creat success!\n");
}//of if
}//of if
int tempFd = open(paraArgc[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(paraArgc[1], F_OK); //判断fifo文件是否存在
if(tempRet == -1){ //若fifo不存在就创建fifo
int tempFIFO = mkfifo(paraArgc[1], 0664);
if(tempFIFO == -1){ //判断文件是否创建成功
perror("mkfifo");
exit(1);
} else{
printf("fifo creat success!\n");
}//of if
}//of if
int tempFd = open(paraArgc[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