当前位置: 代码迷 >> 综合 >> exec函数族接口&&vfork&&分析父子进程在内存中资源问题&&进程之间的通信&&无名管道&&有名管道
  详细解决方案

exec函数族接口&&vfork&&分析父子进程在内存中资源问题&&进程之间的通信&&无名管道&&有名管道

热度:45   发布时间:2024-02-21 00:19:41.0

一、exec函数族接口。
1、什么是exec函数族?
exec函数是一系列的函数接口,然后这些接口的作用就是让一个程序替换(覆盖)掉原来的子进程的。

2、exec函数族接口有哪些?
功能: execl, execlp, execle, execv, execvp, execvpe - execute a file
                            //执行一些文件。

头文件:#include <unistd.h>
原型:
    int execl(const char *path, const char *arg, .../* (char  *) NULL */);
        int execlp(const char *file, const char *arg, .../* (char  *) NULL */);
        int execle(const char *path, const char *arg, .../*, (char *) NULL, 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: 需要执行的那个程序的绝对路径   /home/gec/project
    arg:以","分开所有的参数,以NULL作为结束标识 
    file: 文件名   project
    envp: 环境变量
    argv: 参数的数组

返回值:
    成功:一直执行那个文件。
    失败:只有发生错误时,才会返回-1。

3、示例: 尝试产生一个子进程,然后子进程执行"ls -l"这个程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

//int execvpe(const char *file, char *const argv[],char *const envp[]);


int main(int argc,char *argv[])
{
    //1. 现在还是一个单进程的程序。
    //2. 在单进程的状态下,去创建一个新的子进程。
    pid_t x;
    x = fork();
    
    //3. 通过判断返回值。
    if(x > 0)
    {
        //4. 父进程打印helloworld,就主动回收资源,再退出。
        printf("helloworld!\n");
        
        wait(NULL);
        
        exit(0);
    }
    
    if(x == 0)
    {
        printf("child 1\n");
        
        //5. 子进程使用exec函数族的接口去执行"ls -l"这个程序。
        //execl("/bin/ls","ls","-l",NULL); //为什么这里能执行ls,因为在/bin下找到ls   -> 99%
        //execlp("ls","ls","-l",NULL);  //为什么这里能执行ls,因为ls是在环境变量/bin下
        //execle("/bin/ls","ls","-l",NULL,NULL);
        
        char *arg[3] = {"ls","-l",NULL};
        //execv("/bin/ls",arg);
        //execvp("ls",arg);
        //execvpe("ls",arg,NULL);
        
        printf("child 2!\n");
        
        exit(0);
    }
    
    return 0;
}

4、 结论。
1)以上6个函数功能类似的,记住一个就可以了。
2)exec函数族功能替换(覆盖)掉一个进程,所以在exec函数之后的代码都变成无效的。
3)替换前后,子进程的PID号不会改变。

二、如何确保子进程先运行?  -> vfork()  -> man 2 vfork
功能: vfork - create a child process and block parent
    //创建一个子进程并且让父进程阻塞。

头文件:#include <sys/types.h>
        #include <unistd.h>

原型:
    pid_t vfork(void);

参数:无
返回值:
成功:  产生新子进程                 父进程        子进程
                    子进程的ID号  0

失败:  没有产生新的子进程           父进程
                        -1

注意:
1)fork()虽然父子随机先后,但是同时开始执行。
2)vfork()产生了孩子之后,孩子就会正常执行,但是父进程就会阻塞。
   父进程阻塞到
         1)孩子调用exit()
     2)孩子调用exec()函数族
         3)孩子调用exit(),父进程调用exit()。
     4)孩子不调用exit(),父进程调用exit(),子进程执行结束后,虽然没有exit(),但是父进程中有,也能解锁父进程。
   为止。

3)父子进程都不调用exit()就会出错。
   Aborted (core dumped)

三、分析父子进程在内存中资源问题。
1、研究fork()的资源问题。
例子1: 
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //1. 在fork之前,我定义一个变量。
    int a = 100;
    
    //2. 现在是单进程的程序。
    //3. 在单进程的程序,创建了一个新的子进程。
    pid_t x;
    x = fork();
    
    //4. 通过返回值判断
    if(x > 0)
    {
        printf("parent &a = %p\n",&a); //0x0011
        printf("parent a = %d\n",a);   //100
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        printf("child &a = %p\n",&a);  //0x0011
        printf("child a = %d\n",a);    //100
        exit(0);
    }
    
    return 0;
}

结果:
parent &a = 0x7fffce337c90
parent a = 100
child &a = 0x7fffce337c90
child a = 100

结论:
为什么小孩能够使用a,因为fork()之前的资源,都会给小孩拷贝一份。

例子2:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //1. 在fork之前,我定义一个变量。
    int a = 100;
    
    //2. 现在是单进程的程序。
    //3. 在单进程的程序,创建了一个新的子进程。
    pid_t x;
    x = fork();
    
    //4. 通过返回值判断
    if(x > 0)
    {
        a = 50;
        //printf("parent &a = %p\n",&a); //0x0011
        printf("parent a = %d\n",a);   //50
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        //printf("child &a = %p\n",&a);  //0x0011
        printf("child a = %d\n",a);    //100
        exit(0);
    }
    
    return 0;
}

结果:
parent a = 50
child a = 100

结论: 父进程与子进程拥有独立的空间,父亲的a不会影响小孩的a

例子3:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //1. 在fork之前,我定义一个变量。
    //int a = 100;
    
    //2. 现在是单进程的程序。
    //3. 在单进程的程序,创建了一个新的子进程。
    pid_t x;
    x = fork();
    
    //4. 通过返回值判断
    if(x > 0)
    {
        int a = 100;
        //a = 50;
        //printf("parent &a = %p\n",&a); //0x0011
        printf("parent a = %d\n",a);   
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        //printf("child &a = %p\n",&a);  //0x0011
        printf("child a = %d\n",a);    
        exit(0);
    }
    
    return 0;
}

结果:编译不通过。
原因:子进程的a没有声明。

例子4:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //1. 在fork之前,我定义一个变量。
    //int a = 100;
    
    //2. 现在是单进程的程序。
    //3. 在单进程的程序,创建了一个新的子进程。
    pid_t x;
    x = fork();
    
    //4. 通过返回值判断
    if(x > 0)
    {
        int a = 100;
        //a = 50;
        //printf("parent &a = %p\n",&a); //0x0011
        printf("parent a = %d\n",a);   //100
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        int a = 50;
        //printf("child &a = %p\n",&a);  //0x0011
        printf("child a = %d\n",a);    //50
        exit(0);
    }
    
    return 0;
}

结果:
parent a = 100
child a = 50

==========================================================
综上所述:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //政府有规定,只要生了一个小孩,本来的父亲有多少财产,政府就会补贴一模一样的数据给这个小孩。
    //1. 现在还是只有1个父亲,结果父亲中奖,中了100万。
    int a = 100;
    
    //2. 这时候父亲带着这个100万,去生了一个小孩。
    pid_t x;
    x = fork();
    
    if(x > 0)
    {
        //2.5 父亲生完小孩之后,偷偷去买了一套200平方的房子(fork之后的资源,不会继承给孩子)
        int b = 200;
        
        //3. 父亲查看一下自己的财产,看看有多少钱。
        printf("parent a = %d\n",a); //100
        
        //6. 看到自己有钱了,赶紧去澳门,输了50万。
        a -= 50;
        
        //7. 父亲再次查看自己的财产,看看有多少钱。
        printf("parent a = %d\n",a); //50
        
        //8. 父亲去看看自己的房子,去休息一下。
        printf("parent b = %d\n",b); //200
        b = 300;
    }
    
    if(x == 0)
    {
        //4. 孩子查看一下自己的财产,看看有多少钱。
        printf("child a = %d\n",a); //100
        
        //5. 小孩有点累,想睡眠一下
        sleep(3);
        
        //8. 这时候小孩醒了之后,发现父亲去赌博输钱,赶紧查看自己的财产:
        printf("child a = %d\n",a); //100  心里想着父亲去赌博输钱了,关我屁事  表面父子
        
        //9. 孩子也想去父亲的新房看看。
        printf("child b = %d\n",b); //父亲说,这是我的房子,关你屁事,还要把你告上法庭(编译出错)
    }
    
    return 0;
}

2、研究vfork()资源问题。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //fork(): 父子进程的资源分开的。
    //vfork(): 父子进程的资源共享的。
    
    int a = 0;
    pid_t x;
    x = vfork();
    if(x > 0)
    {
        int b;
        a++;
        printf("parent a = %d\n",a);//3
        printf("parent b = %d\n",b);//0   -> vfork()之后的资源b,不会与子进程的资源b共享。
        wait(NULL);
    }
    
    if(x == 0)
    {
        int b = 200;
        a+=2;
        printf("child a = %d\n",a); //2
        printf("child b = %d\n",b); //200
        exit(0);
    }
    
    return 0;
}

结果:
child a = 2
child b = 200
parent a = 3
parent b = 0

总结fork()与vfork()异同:
1)fork()与vfork()都是可以创建一个新的子进程。
2)fork()与vfork()函数返回值都是一样的。
3)在fork之前的所有资源,在fork()之后,都会拷贝一份给子进程,父子进程拥有独立的空间。
   所以修改了父进程的资源,子进程的资源不会修改。
       修改了子进程的资源,父进程的资源不会修改。
       在父进程中定义的变量,在子进程中不可以使用。
       在子进程中定义的变量,在父进程中不可以使用。
4)在vfork()之前的所有资源,在vfork()之后,可以共享给子进程。    
   所以修改了父进程的资源,子进程的资源跟着修改。
       修改了子进程的资源,父进程的资源跟着修改。
       在父进程中定义的变量,在子进程中不可以使用。
       在子进程中定义的变量,在父进程中不可以使用。
5)fork()随机先后运行。
   vfork()确保子进程先运行,子进程中调用exit()/exec()就会导致父进程解锁。
   
                   fork()      vfork()
分裂之前的资源      分开         共享
分裂之后的资源      分开         分开

四、进程之间的通信。
1、 为什么要学习进程之间的通信?
例如:
./1  -> 开启了一个名字为1的进程。
./2  -> 开启了一个名字为2的进程。
通过学习进程之间的通信,使得不同的进程之间都是可以实现数据的交换。例如进程1发送数据给进程2,进程2收到数据之后,根据数据来做不同的事情。(间接地实现进程1控制进程2)

2、在linux下,进程之间的通信方式有哪些,都有什么特点?
1)管道通信。
管道通信分为有名管道与无名管道,管道是一种特殊的文件,进程通过将数据写入到管道中,另外一个进程从管道中读取数据出来。

2)信号。
在linux下,有非常多信号,例如:暂停,继续,停止..,某一个进程通过发送信号给另外一个进程,从而控制另外一个进程。

3)消息队列。
某一个进程把消息发送到队列上,另外一个进程就可以读取队列上的数据,消息队列好处:进程可以读取队列上某一个特定的数据。

4)共享内存。(信号量)
多个进程访问同一片内存区域。

五、学习进程之间的通信。 --- 无名管道
1、 什么是无名管道?作用机制如何?
无名管道只能作用于亲缘关系之间的进程,例如父子进程。
无名管道其实就是一个数组来的,这个数组有两个端,分别是读端与写端,进程想写入数据到管道中,就往写端中写,如果进程想读取管道中的数据,就读取读端上的数据。

2、使用无名管道步骤:
1)申请数组。
   int fd[2];  -> 里面并不是读端与写端。

2)使用函数初始化数组。  -> pipe()   -> man 2 pipe
初始化数组  -> 初始化数组中读端与写端的值。
功能: pipe, - create pipe
        //创建一条管道

头文件:#include <unistd.h>
原型:
    int pipe(int pipefd[2]);

参数:
    pipefd: 一个具有2个int类型数据的数组。

返回值:
    成功:0
    失败:-1

3)初始化成功
pipefd[0]  -> 读端
pipefd[1]  -> 写端

   例子1: 写一个代码测试无名管道中读端与写端的值是多少。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char *argv[])
{
    //1、 申请数组
    int fd[2] = {0};
    printf("fd[0] = %d\n",fd[0]);
    printf("fd[1] = %d\n",fd[1]);
    
    //2、 使用pipe()去初始化这个数组
    int ret;
    ret = pipe(fd);
    printf("ret = %d\n",ret);
    if(ret == 0)
    {
        //3、 初始化成功后,第一个成员就是读端
        //                    第二个成员就是写端
        printf("init pipe success!\n");
    }
    
    //4、 打印读端与写端的值。
    printf("fd[0] = %d\n",fd[0]);
    printf("fd[1] = %d\n",fd[1]);
    
    return 0;
}

结果:
fd[0] = 0
fd[1] = 0
ret = 0
init pipe success!
fd[0] = 3    -> 读端
fd[1] = 4    -> 写端

   例子2: 尝试使用无名管道,让父子进程之间进行通信。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <string.h>

int main(int argc,char *argv[])
{
    //1、 申请数组并初始化数组
    int fd[2] = {0};
    pipe(fd);
    
    //2、 带着这条无名管道去创建一个新的进程。
    pid_t x;
    x = fork();
    
    //3、 通过fork()函数的返回值去判断。
    if(x > 0)
    {
        //4、 父进程:
        //1) 准备缓冲区
        char buf[20] = {0};
        
        //2) 把需要写入的数据放在缓冲区中。
        fgets(buf,sizeof(buf),stdin);
        
        //3) 将缓冲区中的数据写入到无名管道中的写端即可。
        write(fd[1],buf,strlen(buf));
        
        //4) 主动回收子进程的资源。
        wait(NULL);
        
        //5) 父进程正常退出
        exit(0);
    }
    
    if(x == 0)
    {
        //5、 子进程
            //1) 准备缓冲区
        char buf[20] = {0};
        
        //2) 直接读取无名管道中的读端,将数据读取到缓冲区中。
        read(fd[0],buf,sizeof(buf));
        
        //3) 打印一下读取出来的数据。
        printf("from pipe:%s",buf);
        
        //4) 子进程正常退出
        exit(0);
    }
          
    return 0;
          
}

结果:
gec@ubuntu:/mnt/hgfs/GZ2057/09 系统编程/02/code$ ./pipe_test2
nihao
from pipe:nihao

六、学习进程之间通信。 -- 有名管道
1、什么是有名管道?机制如何?
无名管道是一个数组来的,只能作用于同一个文件中。  -> 只能作用于亲缘关系之间的进程。
有名管道是一个文件来的,文件存在于文件系统中,所以所有的进程都可以看到这个文件。

机制:有名管道的机制.jpg

2、如何创建有名管道?  -> mkfifo()  -> man 3 mkfifo
功能: make a FIFO special file (a named pipe)
    //创建一个特殊的管道文件(命名管道)

头文件:#include <sys/types.h>
       #include <sys/stat.h>

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

参数:
    pathname: 需要创建的管道文件的路径  绝对路径/相对路径   "/home/gec/fifo_test"
    mode:八进制权限   调用mkfifo之前,要先调用umask(0000)设置掩码
          例如: 0777

返回值:
    成功:0
    失败:-1

   例子1: 尝试在家目录创建一个有名管道,名字为fifo_test。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
       
int main(int argc,char *argv[])
{
    //1、 设置掩码为0000
    umask(0000);
    
    //2、 直接创建有名管道,名字为fifo_test。
    int ret;
    ret = mkfifo("/home/gec/fifo_test",0777);
    
    //3、 如果成功,那么就去家目录下看看有没有fifo_test这个文件。
    if(ret == 0)
    {
        printf("mkfifo success!\n");
    }
    
    return 0;
}

结果:
创建成功,并且在家目录下看到fifo_test这个文件。
gec@ubuntu:~$ ls -l fifo_test 
prwxrwxrwx 1 gec gec 0 Sep 21 02:21 fifo_test

   例子2: 尝试使用有名管道,让两个陌生的进程进行通信。

发送端:
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdio.h>

int main(int argc,char *argv[])
{
    //1. 设置掩码。
    umask(0000);
    
    //2. 创建有名管道。
    int ret;
    ret = mkfifo("/home/gec/fifo_test",0777);
    
    //3. 打开有名管道文件。
    int fd;
    fd = open("/home/gec/fifo_test",O_RDWR);
    
    //4. 准备缓冲区
    char buf[100] = {0};
    
    while(1)
    {
        //5. 从键盘中获取数据,存放在缓冲区中。
        fgets(buf,sizeof(buf),stdin);
        
        //6. 将缓冲区的数据写入到文件。
        write(fd,buf,strlen(buf));
        
        //7. 如果发送了一个"quit"到管道文件中,那么发送端程序就结束。
        //   如果不是,则继续发送。
        if(strncmp(buf,"quit",4) == 0)  
        {
            break;
        }
    }
    
    //8. 关闭文件。
    close(fd);
    
    return 0;
}


    

   
 

  相关解决方案