Linux系统调用函数fork()、exec*、wait、system、popen

前言

计算机系统的各种硬件资源是有限的,在现代多任务操作系统上同时运行的多个进程都需要访问这些资源。为了更好的管理有限的资源,所有对这些资源的访问必须由操作系统控制,不允许进程直接操作。因此操作系统提供了使用这些资源的唯一入口----系统调用(System Call),它是操作系统向用户程序提供的一种申请操作系统服务的接口。在linux中系统调用是用户空间访问内核的唯一手段,除异常和陷入外,他们是内核唯一的合法入口。下面将讲解几个常见的进程控制与通信相关的系统调用函数。

关于Linux进程,有以下必要的知识需要了解:

  1. 进程可以看做程序的一次执行过程。在linux下,每个进程有唯一的pid标识进程。pid是一个从1到32768的正整数,其中1一般是特殊进程init,其它进程从2开始依次编号。当用完32768后,从2重新开始。
  2.  linux中有一个叫进程表的结构用来存储当前正在运行的进程。可以使用“ps aux”命令查看所有正在运行的进程。
  3.  进程在linux中呈树状结构,init为根节点

一、fork函数

#include

pid_t fork(void);

作用:创建一个新的进程,让这个进程成为当前进程的子进程。这两个进程执行没有固定的先后顺序,哪个进程先执行要看系统的进程调度策略。

fork函数是一个系统调用,在调用fork函数时,操作系统会复制当前进程的所有信息(包括打开文件、信号处理等),并将其分配给新的进程,但是有些资源是共享的,例如内存映射、消息队列等。新进程与原进程几乎完全相同,但也有略微不同:

  1. pid不同,ppid不同;可以通过getpid()和getppid()函数获取
  2. fork函数的返回值不同

fork的返回值不同:在原进程中,fork返回值为新进程的pid;在新进程中,fork返回值为0;如果出现错误,返回一个负值;因此,在程序中可以根据返回值来判断当前代码运行在哪个进程中。

使用案例:

#include 
#include 
#include 
int main(){
    pid_t pid;
    pid = fork();
    if (pid <0){
        printf("Error: fork failed.");
    } else if (pid ==0){
        printf("This is child process\n");
    } else {
        printf("This is parent process\n");
    }
    return 0;
}

Linux系统调用函数fork()、exec*、wait、system、popen_第1张图片

由运行结果可以看出,当执行完 pid = fork(); 语句后,该语句下面的代码,子进程也会拥有,两个进程都会运行。

下面列举一个典型的例子,加深对fork函数的理解。给出如下c程序,在linux下使用gcc编译:

#include "stdio.h"
 #include "sys/types.h"
 #include "unistd.h"
  int main()
 {
     pid_t pid1;
     pid_t pid2;
     pid1 = fork();
     pid2 = fork();
     printf("pid1:%d, pid2:%d\n", pid1, pid2);
 }

已知从这个程序执行到这个程序的所有进程结束这个时间段内,没有其它新进程执行,请分析:

  • 执行这个程序后,将一共运行几个进程。
  • 如果其中一个进程的输出结果是“pid1:1001, pid2:1002”,写出其他进程的输出结果(不考虑进程执行顺序)。

思考这个问题的关键是正确的区分 pid = fork(); 这段代码的上下两部分。

Linux系统调用函数fork()、exec*、wait、system、popen_第2张图片

如图表示一个含有fork的程序,而fork语句可以看成将程序切为A、B两个部分,然后整个程序会如下运行:

  1. 执行程序,生成进程P1,P1执行完 A 的所有代码
  2. 当执行到pid = fork();时,P1启动子进程 P2。P2继承P1的所有变量、环境变量、程序计数器的当前值。
  3. 在P1中,fork()将 P2 的进程号赋给变量pid,并继续执行 B 代码
  4. 在P2中,将0赋给变量pid,并继续执行 B 代码

参照上面的分析过程,下面一起分析刚刚的两个问题:

  1. 程序执行后,生成一个进程,假设为P0
  2. 执行到pid1 = fork();时,P0启动子进程P1,由题目可知P1的pid为1001
  3. 在P0中,fork返回1001赋值给pid1,继续执行到pid2 = fork();时,启动子进程P2,由题目可知P2的pid为1002
  4. P2生成时,P0中的pid1为1001,P2继承P0的pid1,而作为子进程,pid为0。P2从第二个fork后开始执行,结束后输出“pid1:1001, pid2:0”。
  5. 接着看P1,P1中的pid1 = fork;语句会赋值0给pid1,然后接着执行后面的语句。而后面接着的语句是pid2 = fork();执行到这里,P1又产生了一个新进程,设为P3。
  6. P1中的pid2 = fork;语句会将P3的pid赋值给pid2,P3的pid为1003,所以P1的pid2为1003。P1继续执行后续程序,结束后输出“pid1:0, pid2:1003”。
  7. P3作为p1的子进程,继承P1中pid1=0,并且第二条fork将0赋值给pid,所以P3最后输出“pid1:0, pid2:0”。

因此一共执行了四个进程,P0,P1,P2,P3,另外几个进程的输出分别为:

pid1:1001, pid2:0
pid1:0, pid2:1003
pid1:0, pid2:0

二、exec族函数

#include 

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 execvpe(const char *file, char *const argv[],char *const envp[]);

参数说明:
path:可执行文件的路径名字
arg:可执行程序所带的参数,第一个参数为可执行文件名字,没有带路径且arg必须以NULL结束
file:如果参数file中包含/,则就将其视为路径名,否则就按 PATH环境变量,在它所指定的各目录中搜寻可执行文件。

exec族函数参数极难记忆和分辨,函数名中的字符会给我们一些帮助:
l : 使用参数列表
p:使用文件名,并从PATH环境进行寻找可执行文件
v:应先构造一个指向各参数的指针数组,然后将该数组的地址作为这些函数的参数。
e:多了envp[]数组,使用新的环境变量代替调用进程的环境变量,一般为NULL即可。

exec函数族作用:在进程中启动另一个程序。它可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行成功之后,原调用进程的内容除了进程号外,其他全部都被替换了。

成功无返回值,失败返回 -1。所以通常我们直接在exec函数调用后直接调用perror(),和exit(),无需if判断。

何时使用exec?

  1. 当进程认为自己不能再为系统和用户做出任何贡献了时就可以调用exec函数,让自己执行新的程序
  2. 如果某个进程想同时执行另一个程序,它就可以调用fork函数创建子进程,然后在子进程中调用任何一个exec函数。这样看起来就好像通过执行应用程序而产生了一个新进程一样

常用的也就是下面这两个:

使进程执行某一程序。成功无返回值,失败返回 -1

int execlp(const char *file, const char *arg, ...);		
借助 PATH 环境变量找寻待执行程序
		参1: 程序名
		参2: argv0
		参3: argv1
		...: argvN
		最后:NULL
该函数需要配合PATH环境变量来使用,当PATH所有目录搜素后没有参数1则返回出错。
该函数通常用来调用系统程序。如ls、date、cp、cat命令。

int execl(const char *path, const char *arg, ...);
自己指定待执行程序路径。

这两个函数的作用简单来说就是:execlp(),让当前进程或者子进程执行系统命令,比如:ls,cat,cp等命令;而 execl() 则是执行自己所有的可执行程序,比如一个c程序。 

示例:

#include 
#include
#include
#include

using namespace std;

int main(int argc, char **argv)
{
        cout << "---------------" << endl;
        pid_t pid;
        pid = fork();
        if(pid == -1)
        {
                perror("fork error");
                exit(1);
        }
        else if(pid == 0)
        {
                execlp("ls", "ls", "-l", NULL);  //执行命令:ls -l   
                //函数用法,参数1就是你要执行的命令名称,参数二也是,后面的参数是你执行命令添加的额外参数     
                //如果没有出错,就执行程序,不会有返回值,如果出错就返回错误值
                perror("execlp error");
                exit(1);
        }
        else if(pid > 0)
        {
                sleep(1);
                cout << "my is prent pid = "  << getpid() << endl;
        }
        return 0;
}

三、wait函数

1.僵尸进程与孤儿进程

孤儿进程:父进程结束,子进程被init进程收养。
僵尸进程:子进程结束,父进程没有回收子进程的资源(PCB),这个资源必须要由父进程回收,否则就形成僵尸进程。 

孤儿进程示例:

#include 
#include 
#include 
#include

int main(int argc, char* argv[])
{
	pid_t pid = fork();
	if(pid == 0)
	{
		while(1)
		{
			printf("child: %d, ppid: %d\n", getpid(), getppid());
			sleep(1);
		}
	}
	if(pid > 0)
	{
		printf("parent: %d\n", getpid());
		sleep(3);	
	}
	return 0;
}

 Linux系统调用函数fork()、exec*、wait、system、popen_第3张图片

我们看到,子进程的父进程ID在3秒后变成了1,这说明父进程结束后,它变成了孤儿进程,并被init进程收养,使用kill命令基于可以杀死孤儿进程。

僵尸进程示例:

#include 
#include 
#include 
#include

int main(int argc, char* argv[])
{
	pid_t pid = fork();
	if(pid == 0)
	{
		printf("child: %d, ppid: %d\n", getpid(), getppid());	
		sleep(1);
	}
	if(pid > 0)
	{
		while(1)
		{
			printf("parent: %d\n", getpid());	
			sleep(1);
		}
	}
	return 0;
}

运行后,通过 ps -au 命令查看进程信息:

Linux系统调用函数fork()、exec*、wait、system、popen_第4张图片

图中标出的Z+、defunct都可以表明这是僵尸进程。僵尸进程是不能用kill杀死的,因为kill命令是终止进程,而僵尸进程已经终止了。我们知道僵尸进程的资源需要由父进程去回收,那么我们在这种情况下如何回收僵尸进程的资源呢?方法就是杀死父进程,父进程被杀死后,由init接管子进程并回收子进程资源。

2.wait() 

进程退出时,内核释放该进程大部分资源,包括打开的文件、占用的内存等。但仍保留了该进程的PCB信息,因此需要父进程通过 wait 和 waitpid 函数来进一步回收,否则这些进程会变成僵尸进程,消耗系统资源。

#include 
#include 
 
pid_t wait(int* status);
/*
参数:
    status:存储进程退出时的状态信息;NULL则表示不关心子进程退出状态。
返回值:
    成功:被回收的子进程号
    失败:-1
*/

wait()作用:阻塞等待子进程终止,并获取子进程终止的原因,回收子进程资源。

Linux提供了一些非常有用的宏来帮助解析status,判断子进程终止原因,常用的有:

  • WIFEXITED(status)判断子进程是否正常退出;
  • WIFEXITED(status)为真表示正常退出,使用WEXITSTATUS(status)获取退出状态;
  • WIFEXITED(status)非真,表示非正常退出,使用WIFSIGNALED(status)判断是否被信号杀死;
  • WIFSIGNALED(status)为真,表示是被信号杀死,使用WTERMSIG(status) 获取杀死进程的信号;

使用示例:

#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[])
{
    pid_t pid = fork();
    if(pid == 0)
    {
        printf("child: %d, ppid: %d\n", getpid(), getppid());
        sleep(3); /*子进程睡眠3秒,那么父进程中的wait函数会阻塞3秒,一直等到子进程退出*/
        return 66; /*正常退出,这个值可以被WEXITSTATUS获取到,这个值是有范围的*/
        /*exit(66); 也表示正常退出*/
    }
    if(pid > 0)
    {
        int status;
        pid_t wpid = wait(&status);
        printf("wpid: %d, cpid: %d\n", wpid, pid);
        if(WIFEXITED(status)) /*进程正常退出,获取退出原因*/
        {
            printf("child exit because: %d\n", WEXITSTATUS(status));
        }
        else /*非正常退出*/
        {
            if(WIFSIGNALED(status)) /*为真表示被信号杀死*/
            {
                printf("signal is: %d", WTERMSIG(status));
            }
            else
            {
                printf("other...\n");
            }
        }
        while(1)
        {
            sleep(3);
        }
    }
    return 0;
}

 3.waitpid()

#include 
#include 
 
pid_t waitpid(pid_t pid, int* status, int options);
/*
功能:
    等待子进程结束,回收该子进程资源,并传出子进程退出的状态到status。
参数:
    pid:
        > 0:等待进程号为pid的子进程退出;
        = 0:等待同一个进程组中的任何子进程退出;若子进程已加入其他进程组,则不会等待;
        = -1:等待任意一个子进程退出,此时和等价于wait函数;
        < -1:等待进程组号为pid绝对值的进程组中的任何子进程退出。
    status:存储进程退出时的状态信息;NULL表示不关心子进程退出状态。
    options:
        0:阻塞父进程,等待子进程退出。此时同wait函数。
        WNOHANG:若无任何子进程退出,则立即返回。
        WUNTRACED:若子进程暂停,则立即返回。(少用)
返回值:
    a) 有子进程退出时,waitpid返回已收集到的退出子进程的进程号;
    b) 若options设为WNOHANG,调用waitpid时无子进程退出,则返回0;
    c) 若调用中出错,返回-1,同时设置errno. 
        如当pid对应的进程不存在,或pid对应的进程不是调用waitpid进程的子进程,就会出错,此时errno        
        被设为ECHILD。
*/    

4.回收多个进程

使用wait()

#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[])
{
    int i = 0;
    pid_t pid;
    for(i = 0; i < 5; i++)
    {
        pid = fork();
        if(pid == 0)
        {
            printf("child: %d\n", getpid());
            break;
        }
    }
    sleep(i);
    if(i == 5) /*只有父进程可以执行到i=5*/
    {
        for(i = 0; i < 5; i++)
        {
            pid_t wpid = wait(NULL);
            printf("wpid: %d\n", wpid);
        }
        while(1)
        {
            sleep(1);
        }
    }
    return 0;
}

使用waitpid()

#include 
#include 
#include 
#include 
#include 

int main(int argc, char* argv[])
{
    int i = 0;
    pid_t pid;
    for(i = 0; i < 5; i++)
    {
        pid = fork();
        if(pid == 0)
        {
            break;
        }
    }
    if(i == 5) /*只有父进程可以执行到i=5*/
    {
        printf("parent: %d\n", getpid());
        while(1) /*无限循环保证所有子进程全部回收*/
        {
            pid_t wpid = waitpid(-1/*回收任何子进程*/, NULL, WNOHANG);
            if(wpid == -1)
            {
                break; /*如果返回-1说明已经没有子进程了,退出循环*/
            }
            if(wpid > 0)
            {
                printf("wpid: %d\n", wpid); /*打印被回收的子进程的ID*/
            }
        }
        while(1)
        {
            sleep(1);
        }
    }
    if(i < 5) /*说明是子进程*/
    {
        printf("no. %d child: %d\n", i, getpid());
    }
    return 0;
}

四、system函数

#include 

int system(const char *command);

作用:用于执行一个系统命令或脚本。该函数会调用fork()产生子进程,因此程序将启动一个新的进程来运行。

system函数在Linux上的源码如下:

int system(const char * cmdstring)
{
  pid_t pid;
  int status;

  if(cmdstring == NULL){
      
      return (1);
  }


  if((pid = fork())<0){

        status = -1;
  }
  else if(pid == 0){
    execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
    -exit(127); //子进程正常执行则不会执行此语句
    }
  else{
        while(waitpid(pid, &status, 0) < 0){
          if(errno != EINTER){
            status = -1;
            break;
          }
        }
    }
    return status;
}

常见用法:

  1.  执行外部命令:通过system函数,我们可以在程序中执行外部命令,并获取其输出结果。比如,我们可以使用`system("ls -l")`来列出当前目录下所有文件和文件夹的详细信息。 
  2. 调用脚本文件:system函数也可以用于调用脚本文件,实现一些自动化操作。例如,我们可以使用`system("./script.sh")`来执行一个名为`script.sh`的shell脚本文件。

system函数的返回值有多种情况,通过源码可以发现,其status来自waitpid函数 。判断一个 system 函数调用 shell 脚本是否正常结束的方法应该是如下 3 个条件同时成立:

  1. -1 != status
  2. WIFEXITED(status) 为真
  3. 0 == WEXITSTATUS(status)

使用system函数时要考虑参数的安全性,确保参数是可信的,以防止恶意代码注入。

五、popen函数

在上一章system函数的源码中我们发现,system函数的执行需要通过调用 fork 函数创建一个子进程,子进程再通过execl函数调用shell对传参的可执行文件进行实现。这也意味着system函数实现需要依赖execl函数实现自身功能。因此system函数的结果将直接显示在终端上,这样原本运行的结果就无法在程序中用于实现信息交互等功能。

popen 函数与 system 函数的功能类似,它会调用fork创建一个子进程,之后创建一个连接到子进程的管道,然后读其输出或向其输入端发送数据,可以在程序中获取运行的输出结果。

#include 

FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

参数说明:

commmand:是一个指向以 NULL 结束的 shell 命令字符串的指针。这行命令将被传到 bin/sh 并使用 -c 标志,shell 将执行这个命令。

type:只能是读和写的一种,如果是 “r” 则文件指针连接到command的标准输出,则返回的文件指针是可读的;如果是 “w” 则文件指针连接到command的标准输入,则返回的文件指针是可写的。

stream:popen返回的文件指针。

使用示例:

#include 
 
int main()
{
        FILE *File;
        char readBuf[1024] = {0};
 
        File = popen("ls","r");
        fread(readBuf,1024,1,File);
 
        printf("%s\n",readBuf);
 
        return 0;
}

如果使用popen函数在 “w” 模式下,popen执行完后,仍然可以向管道继续传送命令,继续往标准I/O流中写入内容,直到调用pclose关闭它。

你可能感兴趣的:(#,Linux,linux)