1. 封装fork/wait等操作, 编写函数 process_create(pid_t* pid, void* func, void* arg)( func回调函数就是子进程执行的入口函数, arg是传递给func回调函数的参数)
注:(*fun)()是函数指针, *file是要执行命令函数,argv是执行的命令语句
2. popen/system, 分析这两个函数和fork的区别.
1)fork用来创建一个子进程一个程序
调用fork函数,首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。
事实上,目前大多数的unix系统在实现上并没有作真正的copy。一般的,CPU都是以“页”为单位分配空间的,象INTEL的CPU,其一页在通常情况下是4K字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。
vfork和fork一样,也是创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,不会复制页表。因为子进程会立即调用exec,于是也就不会存放该地址空间。不过在子进程中调用exec或exit之前,他在父进程的空间中运行。
为什么会有vfork,因为以前的fork当它创建一个子进程时,将会创建一个新的地址空间,并且拷贝父进程的资源,而往往在子进程中会执行exec调用,这样,前面的拷贝工作就是白费力气了,这种情况下,而vfork产生的子进程刚开始暂时与父进程共享地址空间(其实就是线程的概念了),因为这时候子进程在父进程的地址空间中运行,所以子进程不能进行写操作,让父进程阻塞,一旦子进程执行了exec或者exit后,才分开。
vfork和fork之间的另一个区别是: vfork保证子进程先运行,在她调用exec或exit之后父进程才可能被调度运行。如果在调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
由此可见,这个系统调用是用来启动一个新的应用程序。其次,子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据。这意味着子进程可能破坏父进程的数据结构或栈,造成失败。
为了避免这些问题,需要确保一旦调用vfork(),子进程就不从当前的栈框架中返回,并且如果子进程改变了父进程的数据结构就不能调用exit函数。子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续。
通常,如果应用程序不是在fork()之后立即调用exec(),就有必要在fork()被替换成vfork()之前做仔细的检查。
用fork函数创建子进程后,子进程往往要调用一种exec函数以执行另一个程序,当进程调用一种exec函数时,该进程完全由新程序代换,而新程序则从其main函数开始执行,因为调用exec并不创建新进程,所以前后的进程id 并未改变,exec只是用另一个新程序替换了当前进程的正文,数据,堆和栈段。
一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。
2)system 可以看做是fork + execl + waitpid
linux下的system()函数:
#include <stdlib.h>
int system(const char* command)
system()函数调用/bin/sh来执行参数的指定命令,/bin/sh一般是一个软连接,指向某个具体的shell,比如bash,-c选项是告诉shell从字符串command中读取命令;在command执行期间,SIGCHLD是被阻塞的,好比在说,hi,内核,这会不要给我送SIGCHLD信号,等我忙完再说;在该command期间,SIGCHLD和SIGQUIT是被忽略的,意思是进程收到这两个信号后没有任何动作。
system()函数执行了三个操作:
- fork一个子进程。
- 在子进程中调用exec函数去执行command;
- 在父进程中调用wait去等待子进程结束;对于fork失败,system函数返回的是-1,如果exec执行成功,也即command顺利执行完毕,则返回command通过exit或return返回的值。(注意command顺利执行不代表执行成功,比如command:“rm 的不够牢固.txt”,不管文件存不存在command都顺利执行了)如果exec执行失败,也即command没有顺利执行,比如信号中断,或者command命令根本不存在,system函数的返回值为1。如果command为NULL,则system函数的返回非0值,一般为1。
源码:
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
int system(const char* cmdstring){//如果cmdstring为空,返回非零值,一般为1
pid_t pid;
int status;
if(cmdstring==NULL){
return (1);
}
if((pid=fork())<0){
status=-1;//fork失败,返回-1
}
else if(pid==0){//子进程
execl("/bin/sh","sh","-c",cmdstring,(char*)0);
exit(127);//exec执行失败返回127,注意exec只在失败时才返回现在的进程,成功的话,现在的进程就不存在了。
}
else
{
while(waitpid(pid,&status,0)<0)
{
if(errno != EINTR)
{
status=-1;//如果waitpid被信号中断,则返回-1
break;
}
}
}
return status;//如果waitpid等待成功,则返回子进程的返回状态。
}
system()函数,功能强大,当system接受的命令为NULL时直接返回,否则fork出一个子进程,因为fork在两个进程:父进程和子进程中都返回,这里要检查返回的pid,fork在子进程中返回0,在父进程中返回子进程的pid,父进程使用waitpid等待子进程结束,子进程则是调用execl来启动一个程序代替自己,execl(“/bin/sh”, “sh”, “-c”, cmdstring, (char*)0)是调用shell,这个shell的路径是/bin/sh,后面的字符串都是参数,然后子进程就变成了一个shell进程,这个shell的参数是cmdstring,就是system接受的参数。在windows中的shell是command。
什么时候system()函数的返回值0,只有command命令返回0时。
那么如何监控system()函数执行状态
system函数用起来容易出错,返回值太多,而且返回值很容易跟command的返回值混淆,这里我们推荐的是popen函数进行代替,关于popen函数的简单使用。popen()函数较于system()函数的优点在于使用简单,popen()函数只返回两个值。成功返回子进程的status,使用WIFEXITED相关宏就可以取得command的返回结果;失败返回-1,我们可以使用perro()函数或者strerror()函数得到有用的错误信息。
3)popen()也常常被用来执行一个程序
在标准I/O函数库提供了popen函数,它启动另外一个进程去执行一个shell命令,这里我们称调用popen的进程为父进程,由popen启动的进程称为子进程。popen函数还创建一个管道用于父子之间的通信,父进程要么从管道中读取信息,要么想管道中写信息,至于是读还是写,取决于父进程调用popen函数时传递的参数。
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
//函数的功能:popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令,参数type可食用“r”代表读取,“w”代表写入。依照此type的值,popen会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可以利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中,返回值:若成功则返回文件指针,否则返回NULL,错误原因存在于errno中。
函数功能:pclose()函数用来关闭由popen所建立的管道及文件指针。参数stream为先由popen()所返回的指针文件。
返回值:若成功返回shell的终止状态(也即子进程的终止状态),若出错则返回-1,错误原因在errno中。
取得当前目录下的文件个数,在shell下我们可以使用:
ls | wc -l
下面我们来看看popen的使用:
popen() 函数用创建管道的方式启动一个 进程, 并调用 shell。因为管道是被定义成单向的, 所以 type 参数只能定义成只读或者只写, 不能是两者同时, 结果流也相应的是只读或者只写. command 参数是一个字符串指针, 指向的是一个以 null 结束符结尾的字符串, 这个字符串包含一个 shell 命令. 这个命令被送到 /bin/sh 以 -c 参数执行, 即由 shell 来执行. type 参数也是一个指向以 null 结束符结尾的字符串的指针, 这个字符串必须是 ‘r‘ 或者 ‘w’ 来指明是读还是写。
popen() 函数的返回值是一个普通的标准I/O流,它只能用 pclose() 函数来关闭, 而不是 fclose() 函数。 向这个流的写入被转化为对 command 命令的标准输入;而 command 命令的标准输出则是和调用 popen(),函数的进程相同,除非这个被command命令自己改变。相反的,读取一个 “被popen了的” 流, 就相当于读取 command 命的标准输出, 而 command 的标准输入则是和调用 popen, 函数的进程相同。注意, popen 函数的 输出流默认是被全缓冲的。pclose 函数等待相关的进程结束并返回一个 command 命令的退出状态,就像 wait4 函数 一样
代码示例:
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
#include <string.h>
#define MAXLINE 1024
int main()
{
char result_buf[MAXLINE],command[MAXLINE];
int rc=0;//用于接收命令返回值
FILE* fp;
//将要执行的命令写入buf
snprintf(command,sizeof(command),"ls ./ |wc -l");
//执行预先设定的命令,并读出该命令的标准输出
fp=popen(command,"r");
if(NULL==fp){
perror("popen执行失败");
exit(1);
}
while(fgets(result_buf,sizeof(result_buf),fp)!=NULL)
{
if('\n'==result_buf[strlen(result_buf)-1]){
result_buf[strlen(result_buf)-1]='\0';
}
printf("命令【%s】输出【%s】\r\n",command,result_buf);
}
//等待命令执行完毕并关闭管道和文件指针
rc=pclose(fp);
if(-1==rc){
perror("关闭文件指针失败");
exit(1);
}
else{
printf("命令【%s】 子进程结束状态【%d】命令返回值【%d】\r\n",command,rc,WEXITSTATUS(rc));
}
return 0;
}
运行结果如下:
上面popen只是捕获了command的标准输出,如果command命令执行失败了,子进程会将错误的信息打印到标准错误输出。父进程就无法获取,比如command命令为“ls nofile.txt”,事实上我们根本没有nofile.txt这个文件,这时shell会输出“ls :nofile.txt:No such file or directory”。这个输出是在标准错误上输出上的。通过上面的程序无法获取。
注意:如果你把上面的程序中的command设成“ls nofile.txt”,编译执行程序你会看见如下的结果:
需要注意的是输出的第一行并不是父进程的输出,而是子进程的标准错误输出。有时候子进程的错误信息是很有用的,那么父进程该如何获取错误信息呢?
在这里我们可以重定向子进程的错误输出,让错误重定向到标准输出(2>&1),这样父进程就可以捕捉子进程的错误信息。例如:command为“ls nofile.txt 2>&1”,输出如下:
附加:
子进程的终止状态判断设计到宏,设终止状态为status。
WIFEXITED(status) 如果子进程正常结束则为非0值。
WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用WIFEXITED来判断是否正常结束才能使用该宏。
WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真。
WTERMSIG(status)取得子进程因信号而终止的信号代码,一般会先用WIFSIGNALED来判断后,才使用此宏。
WIFSTOPPED(status)如果子进程处于暂停执行情况,则此宏值为真,一般只有使用WUNTRACED时才会有此情况。
WSTOSIG(status)取得引发子进程暂停的信号代码,一般会先用WIFSTOPPED来判断后才能使用此宏。