在了解了进程的相关概念之后,接下来了解一下进程控制。
一. 进程的创建
以下是两种创建子进程的方法:
1. 通过系统调用fork
该系统调用的原型为:
pid_t fork(void);//需引用的头文件为<unistd.h>
fork的作用是通过正在运行的父进程来创建子进程。fokk在执行过程中:
(1)系统分配新的内存块和数据结构给子进程,其中子进程与父进程对代码进行共享,数据进行写实拷贝。
(2)将父进程的部分数据结构内容拷贝给子进程
(3)使子进程添加到系统进程列表中等待调度。
返回值为:
(1)如果fork调用失败,即子进程创建失败,返回-1;
(2)如果子进程创建成功,给父进程返回子进程的pid,给子进程返回0。
fork之后一般用if-else语句分流来使父,子进程执行不同的代码段或程序。通过下面的代码来演示一下fork:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork");
exit(1);
}
else if(pid > 0)//父进程
{
printf("father:pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
else//子进程
{
printf("child:pid = %d,ppid = %d\n",getpid(),getppid());
}
return 0;
}
运行结果如下:
[admin@localhost control.c]$ gcc fork.c
[admin@localhost control.c]$ ./a.out
father:pid = 12943,ppid = 2641
child:pid = 12944,ppid = 12943
在该例中,子进程创建好后,先执行父进程,在执行子进程。
注意:fork之后,父,子进程那个先运行是不一定的,由调度器决定。
2. 通过系统调用vfork
vfork的作用和返回值与fork类似。但是,有以下几点需要区别:
(1)vfork创建的子进程与父进程共享虚拟地址空间,所以代码和数据均共享
(2)vfork之后子进程先运行,在子进程调用exec或exit之后,父进程才可能运行。
vfork演示:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
int g_val = 100;//设置全局变量
pid_t pid = vfork();
if(pid == -1)
{
perror("vfork");
exit(1);
}
if(pid > 0)//父进程
{
printf("father:pid = %d,g_val = %d,ret= %d\n",getpid(),g_val,pid);
printf("%p\n",&g_val);
}
else//子进程
{
g_val = 50;
printf("child:pid %d,g_val = %d,ret = %d\n",getpid(),g_val,pid);
printf("%p\n",&g_val);
exit(0);
}
return 0;
}
该程序运行起来之后,先睡眠了5s,在输出下面的结果:
child:pid 14150,g_val = 50,ret = 0
0xbff0e288
father:pid = 14149,g_val = 50,ret= 14150
0xbff0e288
可以观察到:
(1)子进程在睡眠期间,父进程并没有输出,等子进程exit退出之后,父进程才输出。说明子进程先运行。
(2)子进程将全局变量g_val改变之后,父进程输出的也是改变之后的值,同时两进程中分别输出全局变量的地址,发现是相同的,说明父,子进程指向同一片虚拟内存空间,所以它们共享代码和数据。
将上述代码中子进程的exit去掉,运行结果如下:
[admin@localhost control.c]$ ./a.out
child:pid 13041,ppid = 13040,g_val = 50
father:pid = 13040,ppid = 2641,g_val = 50
child:pid 13042,ppid = 13040,g_val = 50
father:pid = 13040,ppid = 2641,g_val = 50
child:pid 13043,ppid = 13040,g_val = 50
father:pid = 13040,ppid = 2641,g_val = 50
child:pid 13044,ppid = 13040,g_val = 50
father:pid = 13040,ppid = 2641,g_val = 50
......
此时,子进程会return返回,而main()函数return后,通常会调用exit()或相似的函数,然后父进程执行。此时会再次调用main(),父进程一直创建子进程,于是进入一个无限循环的结果。
二,进程终止
一个进程结束方式有以下几种:
(1)正常退出,结果正确
(2)正常退出,结果错误
(3)异常退出。
1. 进程正常退出方式
(1)通过在main函数中调用return(只有main函数中调用return,进程才会退出,若在其他函数中调用,进程不会退出)
(2)在程序的任意位置调用exit(无论在main函数中,还是在其他函数中调用exit进程都会退出);
(3)在程序的任意位置调用_exit(无论在main函数中,还是在其他函数中调用exit进程都会退出);
程序正常退出后,运行的结果如何,是否正确。此时可以通过:echo $?命令来查看进程的退出码。
注意:echo只会保存最近一个进程的退出码
(1)如果退出码为0,表示结果正确;
(2)如果退出码非0,表示结果错误。此时可根据退出码来打印出相应的错误码描述,进而得知错误原因。例如:
[admin@localhost control.c]$ ls 378
ls: cannot access 378: No such file or directory//错误描述
[admin@localhost control.c]$ echo $?
2//退出码
在上述情况下直接给出了错误描述。而在某些情况下,只会给出退出码,此时如何将退出码转换为错误描述呢?系统中有132条错误描述,每个错误描述对应一个数字。所以可以根据退出码即数字调用strerror函数找到对应的错误描述。
exit函数
函数原型
void exit(int status);//头文件<unistd>
在系统调用该函数后,首先会执行用户定义的清理函数,清空缓冲区,关闭流等。再使程序退出。
_exit函数
函数原型
void _exit(int status);//头文件<unistd>
该函数与exit的区别是在调用它之后,直接是程序退出。不做前期的处理。
例如:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
printf("hello world");
sleep(3);
exit(1);//调用exit函数
}
程序运行后,先睡眠了3s,在输出结果:
[admin@localhost control.c]$ ./a.out
hello world[admin@localhost control.c]$
因为printf会将“hello wold”先放入输出缓冲区中,在执行之后的语句,当执行到exit,exit会先将缓冲区中的内容输出,再结束进程。
将程序中的exit替换为_exit之后,程序开始运行后,先睡眠了3s,然后直接退出。
[admin@localhost control.c]$ ./a.out
[admin@localhost control.c]$
因为printf会将“hello wold”先放入输出缓冲区中,在执行之后的语句,当执行到_exit,_exit不会先将缓冲区中的内容输出,而是直接结束进程。
注意:exit和_exit参数中的status虽然是整数,但只有低8位可以被父进程使用。所以status中的低8位是多少,其退出码即为多少,所以调用exit或_exit后退出码的取值范围是0~255。
return
在main函数中执行return n与执行exit(n)同效。
2. 异常终止
异常终止是程序还没运行完,就结束了。它是操作系统向进程发送一个信号,是进程强制终止。
三. 进程等待
父进程创建了子进程,子进程退出后,若父进程继续做自己的事,对子进程不管不问,此时,
(1)子进程就会变为僵尸进程,僵尸进程用kill是杀不掉的,此时就会造成内存泄漏。
(2)同时父进程交给子进程的任务完成的如何,即子进程的退出状态如何,这些都是未知的。
所以,父进程需要通过进程等待的方式,等待子进程完成,获取其退出信息并释放资源。
进程等待的方法
1. 通过系统调用wait
wait的函数原型如下:
pid_t wait(int* status);//头文件<sys/types.h>和<sys/wait.h>
该系统调用的作用是等待子进程退出。将子进程的退出信息保存在status中,然后系统释放子进程相关资源。其中:
(1)返回值:如果wait调用失败,返回-1;如果调用成功,返回等待的子进程的pid。
(2)参数:
如果参数为空,表示不关心退出信息。
如果参数不为空,操作系统会将退出信息保存在status中。status被当做位图来看待,其中:
1) status的低7位存放信号信息。即子进程是否正常退出。如果是正常退出,则这几位为0;如果是异常终止,这几位存放的就是引起子进程异常终止的信号值。
2) status的次低8位存放的是子进程的退出码。如果子进程正常终止,那么子进程执行的结果是正确还是错误,可以查看退出码。如果退出码为0,则表示结果正确,若退出码不为零,则结果错误,可以通过退出码来查看相应的错误信息。如果子进程异常终止,则不关心退出码。
wait代码演示:
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#include<errno.h>
int main()
{
pid_t ret = fork();//创建子进程
if(ret == -1)
{
perror("fork");
exit(1);
}
else if(ret > 0)//父进程
{
printf("father:pid = %d,ret = %d\n",getpid(),ret);
int st;
printf("father is waiting\n");
int ret1 = wait(&st);//父进程等待子进程退出
if(ret1 == -1)//如果wait调用失败
{
printf("wait failed,wait return %d\n",ret1);
}
else//wait调用成功
{
printf("wait success,wait return %d\n",ret1);
if((st & 0x7f) == 0)//,如果st的低7位等于0,即子进程正常退出
{
printf("child exit normal,sig code:%d,exit code:%d\n",st & 0x7f,(st >> 8) & 0xff);//打印退出码
}
else//如果低7位不为0,即子进程异常退出
{
printf("child exit unnormal,sig code:%d\n",st & 0x7f);//打印引起子进程异常退出的信号值
}
}
printf("father quit\n");//子进程退出之后,父进程才退出
}
else//子进程在运行
{
printf("child is running\n");
printf("child:pid = %d,ret = %d\n",getpid(),getppid(),ret);
int count = 5;
while(count--)
{
printf("count = %d\n",count);
sleep(2);
}
printf("child quit\n");
exit(1);//令子进程的退出码为1
}
return 0;
}
运行结果如下:
father:pid = 15118,ret = 15119
father is waiting
child is running
child:pid = 15119,ret = 15118
count = 4
count = 3
count = 2
count = 1
count = 0
child quit
wait success,wait return 15119
child exit normal,sig code:0,exit code:1
father quit
此时,信号值为0,所以子进程正常退出。退出码为1。
当该程序运行起来后,在另一终端用kill命令杀死子进程:
[admin@localhost ~]$ kill -9 15140
运行结果如下:
[admin@localhost control.c]$ ./a.out
father:pid = 15139,ret = 15140
father is waiting
child is running
child:pid = 15140,ret = 15139
count = 4
count = 3
count = 2
count = 1
count = 0
wait success,wait return 15140
child exit unnormal,sig code:9
father quit
此时,子进程异常终止,信号值为9。
2. 通过系统调用waitpid
函数原型:
pid_t waitpid(pid_t pid, int *status, int options);//头文件<sys/types.h>和<sys/wait.h>
waitpid的作用与wait相同。其中
(1)参数pid:父进程指定等待的子进程的pid。如果父进程等待任意一个子进程,该参数为-1。
(2)参数status:与wait中的status作用和用法相同。另外:
参数status中的信号信息和退出码还可由以下方式获得:
1)WIFEXITEND(status):判断子进程是否正常终止,如果正常终止,则返回非零值,如果异常终止,则返回0
2)WEXITSTATUS(status):显示子进程的要退出码
上述方法也可用于wait函数中的status。
(3)参数options:表示父进程以何种方式等待。
1)以阻塞式等待,该参数为0。此时父进程除了等待,不能做别的事情(wait默认以阻塞式等待,故与wait同效)。
2)如果以非阻塞式等待,该参数为WNOHANG。
若子进程还没退出,则返回值为0。此时,父进程可以做其他的事,但父进程要一直以轮询的方式查看子进程是否结束。
若子进程退出了,返回值为所等待进程的pid。
(4)返回值:
1)waitpid调用失败,返回值为-1;
2)waitpid调用成功。
若父进程以阻塞式等待,则子进程退出后,返回所等待的子进程的pid;
若父进程以非阻塞式等待,若子进程还没退出,则返回值为0。若子进程退出了,返回值为所等待进程的pid。
代码演示:
(1)父进程阻塞式等待(与wait作用相同)
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid > 0)//父进程
{
printf("father is running\n");
printf("father:pid = %d\n",getpid());
printf("father is waiting\n");
int st;
int ret = waitpid(-1,&st,0);//阻塞式等待任意进程,注意与wait的用法做区别
printf("wait end\n");
if(ret == -1)
{
printf("wait failed\n");
}
else
{
printf("wait success!,wait return %d\n",ret);
if(WIFEXITED(st))//可替换为:if(!(st&0x7f))
{
printf("child exit normal! exit code of child is %d\n",WEXITSTATUS(st));
}
else
{
printf("childe exit inormal! sig code of child is %d\n"st&0x7f);
}
}
}
else//子进程
{
printf("child is running\n");
printf("child:pid = %d\n",getpid());
int count = 5;
while(count--)
{
printf("count = %d\n",count);
sleep(3);
}
exit(20);
}
return 0;
}
上述程序与wait演示代码中的运行结果相同。
(2)父进程非阻塞式等待
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
printf("%s fork error\n",__FUNCTION__);
exit(1);
}
else if(pid > 0)//父进程
{
printf("father is running\n");
printf("father:pid = %d\n",getpid());
printf("father is waiting\n");
int ret;
int st;
do
{
ret = waitpid(-1,&st,WNOHANG);//非阻塞式等待
if(ret == 0)//子进程没有结束,父进程就做其他事情
{
printf("child is running:\n");//此时,父进程所做的其他事情就是一直输出该语句
sleep(1);
}
}while(ret == 0);
printf("wait end\n");
if(WIFEXITED(st))
{
printf("child exit normal,the exit code of child is %d\n",WEXITSTATUS(st));
}
else
{
printf("child exit inormal,the exit sig of child is %d\n",st&0x7f);
}
}
printf("father quit\n");
}
else//子进程
{
printf("child:pid = %d\n",getpid());
sleep(5);
printf("child quit\n");
exit(50);
}
return 0;
}
运行结果:
[admin@localhost control.c]$ ./a.out
father is running
father:pid = 15528
father is waiting
child is running:
child:pid = 15529
child is running:
child is running:
child is running:
child is running:
child quit
wait end
child exit normal,the exit code of child is 50
father quit
此时,子进程正常退出,子进程的退出码是50。
当该程序运行起来后,在另一终端用kill命令杀死子进程:
admin@localhost ~]$ kill -9 15553
运行结果如下:
[admin@localhost control.c]$ ./a.out
father is running
father:pid = 15552
father is waiting
child is running:
child:pid = 15553
child is running:
child is running:
child is running:
wait end
child exit inormal,the exit sig of child is 9
father quit
此时,子进程异常退出,退出信号为9。
四. 进程的程序替换
上述例子中父进程fork创建子进程后,子进程去执行与父进程不同的代码段。那如果子进程要执行不同的程序呢?这时,子进程通过调用exec函数去执行另一程序。子进程在执行另一程序时,只是将另一程序的代码和数据拷贝到属于子进程的物理内存中,然后从新程序的启动历程开始执行。因为并没有创建新的进程,所以exec前后子进程的pid并没有改变。
以下是exec函数的一种表示,可以用man exec来查看:
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 env[]);
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 env[]);//头文件均为<unistd>
在调用这些函数后子进程就去执行执行的程序,如果调用成功,则不再返回,如果调用失败,则返回-1。其中:这些函数第一个参数表示的均为子进程要去执行什么文件或命令,第二个参数表示以何种方式执行。
根据函数名区别函数的不同:
(1)l:表示执行方式以可变参数列表的形式呈现
(2)v:表示执行方式以指针数组的形式呈现
(3)p:表示子进程执行的程序要根据系统自带的环境变量PATH中查找路径;
(4)e:表示要自己维护环境变量
代码演示:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
int main()
{
pid_t pid = fork();
if(pid == -1)
{
printf("%s fork error\n",__FUNCTION__);
exit(1);
}
else if(pid > 0)//父进程
{
int st;
printf("father is waiting\n");
int re = wait(&st);//父进程等待
if(re == -1)//等待失败
{
printf("father:wait failed\n");
exit(1);
}
printf("father quit\n");//等待成功
}
else//子进程
{
char *const arg[]={"ls","-a","--color=auto",NULL,"-l"};
char *const env[]={"PATH=/bin",NULL};
// int ret = execl("/bin/ls","ls","-a","-l","--color=auto",NULL);//exec函数的几种调用形式
// int ret = execlp("ls","ls","-a","-l","--color=auto",NULL);
// int ret = execle("/bin/ls","ls","-a","-l","--color=auto",NULL,env);
int ret = execv("/bin/ls",arg);
// int ret = execvp("ls",arg);
// int ret = execve("/bin/ls",arg,env);
if(ret == -1)//exec函数调用失败
{
printf("execl error\n");
}
printf("child quit\n");//如果调用失败,就会输出该语句,否则不输出
exit(1);
}
return 0;
}
注意:可变参数列表和指针数组均以NULL结尾
运行结果:
[admin@localhost control.c]$ ./a.out
father is waiting
. .. a.out echo.c exec.c exit.c fork.c myshell.c t.c test.c vfork.c wait.c waitpid.c
father quit
上述结果并没有输出“child quit”语句,说明子进程在调用exec函数直接去执行指定的程序,并没有返回。
将上述的“/bin/ls”改为"/bin/l",结果如下:
[admin@localhost control.c]$ ./a.out
father is waiting
execl error
child quit
father quit
此时,因为子进程并没有找到执行的程序而是exec函数调用失败,从而返回到exec调用处继续执行之后的语句,所以输出了"child quit"。
上述即说明了如果exec调用成功,没有返回值,调用失败才会返回。
五. 实现简易的shell
在linux中输入一条命令,shell就会执行相应的命令,并输出相关的结果。这里,利用进程的程序替换来实现建议的shell。
例如要执行"ls -l -a"命令。要使子进程执行这些命令,可以调用exec函数,上述有六种exec函数,具体要选用哪个呢?
(1)因为在执行exec之前并不知道要执行的程序和对应的执行方式。所以不能将其一一列举出来,故不能用可变参数列表,而要用指针数组的形式实现。提前定义一个指针数组,将输入的命令以空格为分隔符,分隔各字符串,作为执行程序的方式选项。(2)而该数组的第一个字符串如ls即为要执行的程序名,但不知道其路径,所以要在环境变量path中查找。
所以要用execvp函数。
接下来来一步步实现shell:
(1)提取命令
(2)创建子进程,使子进程执行exec函数
(3)一直循环执行上述过程
代码如下:
#include<stdio.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<stdlib.h>
char* arg[20] = {};
int count = 0;
//命令解析函数
void do_parse(char* buf)
{
int i = 0;
int status = 0;//定义状态变量,0表示为空格状态,1表示非空格状态
for(count = i = 0;buf[i] != '\0';i++)
{
if((!isspace(buf[i])) && (status == 0))//当遇到非空格且状态原状态为0,及表示有空格进入到非空个时
{
arg[count++] = buf + i;//将字符串放入arg中
status = 1;//此时状态变为非空格状态1
}
else if(isspace(buf[i]))//如果遇到空格
{
buf[i] = 0;//使空格处为0,作为字符串的结束符
status = 0; //此时,状态变为空格状态0
}
}
arg[count] = NULL;//提取完命令中的各字符后,使最后一个为NULL
}
//创建子进程
void do_fork()
{
pid_t pid = fork();
if(pid == -1)
{
perror("fork error");
exit(1);
}
else if(pid == 0)//子进程
{
int ret = execvp(arg[0],arg);//使子进程执行exec函数,arg[0]表示要执行的命令,arg表示以何种方式执行
if(ret == -1)
{
printf("execv error\n");
exit(1);
}
}
else//父进程
{
int st;
int ret1 = wait(&st);//父进程等待
if(ret1 == -1)
{
printf("wait failed\n");
exit(1);
}
}
}
int main()
{
char buf[1024] = {};
while(1)
{
printf("myshell# ");
fgets(buf,sizeof(buf),stdin);//获取命令
do_parse(buf);//解析命令
do_fork();//创建子进程
}
return 0;
}
运行结果:
myshell# ls
a.out echo.c exec.c exit.c fork.c myshell.c t.c test.c vfork.c wait.c waitpid.c
myshell# pwd
/home/admin/bit_code/Linux/control.c
myshell#
该shell程序只能支持最基本的命令,在管道,重定向等方面的执行还有待改进。
六. 封装fork/wait等函数
封装后的函数是这样的:
void process_create(pid_t* pid, void (*func)(void*), void* buf);
在该函数中,通过fork创建一个子进程,使子进程去执行回调函数func,buf为回调函数的参数。其中参数pid是输出型参数,用于保存子进程的的pid。父进程wait去等待子进程退出。
假设buf是字符串,关于字符串可进行很多不用的操作,所以为方便起见,将关于该字符串的操作封装为回调函数。当关于该字符串的操作改变时,执行修改相应的回调函数即可,而不需要修改封装函数process_create,这样可以提高代码的维护性。
下面,通过两个例子,来说明该封装函数(关于fork,wait,exec等函数的用法在上文已说明,这里就不细说了):
(1)创建子进程,使子进程调用func函数将字符串的内容输出
int main()
{
pid_t pid = 0;//定义输出型变量,用于接收子进程的pid
char buf[100] = "hello world";//子进程要执行的回调函数的参数,在解析字符串时会改变字符串的内容,所以定义为数组的形式
process_create(&pid,func,buf);//创建子进程,使子进程调用func函数将字符串buf输出
printf("child:pid = %d\n",pid);
return 0;
}
所以在process_create中用fork创建一个子进程,使子进程去执行回调函数func,父进程通过wait等待子进程退出:
//该函数用于创建一个子进程,使子进程去执行回调函数func,buf为回调函数的参数
void process_create(pid_t* pid, void (*func)(void*), void* buf)
{
pid_t ret1 = fork();//创建子进程
if(ret1 == -1)//子进程创建失败
{
perror("fork");
exit(1);
}
else if(ret1 > 0)//父进程
{
*pid = ret1;//*pid接受子进程的pid
int status;
int ret = wait(&status);//父进程等待
if(ret == -1)//等待失败
{
perror("wait");
exit(1);
}
else//等待成功
{
if(WIFEXITED(status))//子进程正常终止
{
printf("exit code %d\n",WEXITSTATUS(status));
}
else//子进程异常终止
{
printf("sig code %d\n",status & 0x7f);
}
}
}
else//子进程
{
(*func)(buf);//使创建好后的子进程去执行回调函数
exit(0);//执行结束后,子进程终止
}
}
而在回调函数中func中,子进程要输出其参数字符串:
func为子进程要执行的回调函数,参数为buf
void func(void* buf)
{
printf("%s\n",(char*)buf);//使子进程输出参数字符串
}
运行结果如下:
[admin@localhost control.c]$ gcc process_create.c
[admin@localhost control.c]$ ./a.out
hello world
exit code 0//子进程的退出码
child:pid = 21156//子进程的pid
(2)创建子进程,使子进程调用func函数去执行字符串所表示的shell命令
例如,字符串为“ls -a”,使子进程去执行该命令。
只需在main函数中将字符串buf的内容改为:
char buf[100] = "ls -a";
并将回调函数改为:
//func为子进程要执行的回调函数,参数为buf
void func(void* buf)
{
char* buf1 = (char*)buf;//类型强转
do_parse(buf1);//解析参数字符串
int r = execvp(arg[0],arg);//子进程调用execvp函数去执行其他的程序
if(r == -1)//execvp调用失败
{
perror("execvp");
exit(1);
}
其中,func函数中的do_parse函数即为上述“简易shell函数”中的命令解析函数,arg,count也为“简易shell函数”中定义的全局变量。
运行结果如下:
[admin@localhost control.c]$ gcc process_create.c
[admin@localhost control.c]$ ./a.out
. .. a.out echo.c exec.c exit.c fork.c myshell.c popen.c process_create.c system.c test.c vfork.c wait.c waitpid.c
exit code 0//子进程的退出码
child:pid = 21180//子进程的PID
所以,之后再使子进程执行不同的字符串或操作时,只需改变传入的参数字符串和回调函数即可。而不必再封装函数中做任何修改。
七. popen/pclose函数
函数原型:
FILE* popen(const char* command,const char* type);
int pclode(FILE* stream);//头文件均为<stdio.h>
1. popen函数
(1)popen函数通过fork创建一个子进程,然后在子进程中调用exec函数执行参数command表示的指令。调用popen的为父进程,由popen启动的进程为子进程。
(2)同时,popen会创建一个管道,用连接子进程的标准输入和标准输出设备。然后返回一个文件指针。
(3)参数type有两种:
"r":读,根据文件指针从管道读内容。子进程将命令执行的结果放入标准输出,父进程通过管道读取内容。
"w":写,根据文件指针向管道写内容,此时,父进程通过管道向子进程的标准输入设备中写内容,然后子进程去执行命令。
(4)返回值:如果popen函数调用成功,返回一个标准的I/O文件指针,通过文件指针来对管道的内容进行读写。如果失败,返回NULL
2. pclose函数
popen函数在执行过程中,不等待shell命令执行完成就返回了,所以子进程的退出状态就有pclose来获取。所以pclose的作用是关闭管道。成功返回子进程的终止状态,失败返回-1。
代码演示1:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
FILE* fp = popen("ls -a","r");//以读方式从管道中读取内容
if(fp == NULL)
{
perror("popen");
exit(1);
}
char buf[4096];
fread(buf,1,sizeof(buf),fp);//通过文件指针fp将管道中的内容写入buf中
printf("%s\n",buf);
pclose(fp);
return 0;
}
运行结果:
[admin@localhost control.c]$ gcc popen.c
[admin@localhost control.c]$ ./a.out
.
..
a.out
echo.c
exec.c
exit.c
fork.c
myshell.c
popen.c
代码演示2:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
int main()
{
FILE* fp = popen("cat","w");//以写方式打开管道
if(fp == NULL)
{
perror("popen");
exit(1);
}
fwrite("hello world\n",1,strlen("hello world"),fp);//向管道中写入字符串“hello world”,在由子进程执行cat输出到屏幕上
pclose(fp);
return 0;
}
运行结果:
[admin@localhost control.c]$ gcc popen.c
[admin@localhost control.c]$ ./a.out
hello world
7. system函数
函数原型:
int system(const char* command);//头文件<stdlib.h>
该函数的功能也是执行shell命令,其中,在执行过程中:
(1)会fork一个子进程
(2)在子进程中调用exec函数去执行command命令,同时父进程调用wait等待子进程调用结束
(3)返回值:
若fork失败,返回-1
若exec执行失败,返回127
若exec执行成功,即command命令执行成功,返回command命令的退出码
如果command为NULL,返回非零值。
(4)在system执行过程中,信号SIGCHLD,SIGINT,SIGQUIT是被忽略的。
注意:system执行时,父进程必须等待子进程退出。
popen执行时,未等待子进程完成就退出,子进程的退出状态通过pclose来获取,所以,popen和pclose要成对出现。
代码演示:
#include<stdlib.h>
#include<stdio.h>
int main()
{
system("ls");
return 0;
}
运行结果:
[admin@localhost control.c]$ gcc system.c
[admin@localhost control.c]$ ./a.out
a.out exec.c fork.c popen.c system.c vfork.c waitpid.c
echo.c exit.c myshell.c process_create.c test.c wait.c
注意:popen本身是不阻塞的,要通过标准io读取是它阻塞。而system本身是阻塞的。