第二十八篇,关于exec函数族函数接口, vfork() 函数的使用,研究vfork()父子进程的资源问题,进程之间的通信方式——无名管道,有名管道,信号的详细解答

一、exec函数族函数接口。
1、什么是exec函数族?
指的是一堆可以帮我们执行程序的函数接口。

2、exec函数族函数接口作用?
让一个新的程序替换到子进程,让新的程序作为子进程,PID号不会变。


使用格式:
         #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: 需要替换的那个程序的绝对路径。
    arg: 以","分开所有的参数,以NULL作为结束标志。
    file:程序的文件名
    envp:环境变量
    argv:以","分开所有的参数,以NULL作为结束标志。  --> 所有的参数都要放在一个数组中。

返回值:
    只有发生错误时候才会返回-1 


    例子: 让子进程执行"ls -l"这个程序。

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
// "ls -l" 替换 子进程

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


int main(int argc,char *argv[])
{
    pid_t x;
    x = fork();
    if(x > 0)
    {
        sleep(2);
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        printf("before exec!\n");
        //execl("/bin/ls","ls","-l",NULL);   --> 一般只记忆第一个
        //execlp("ls","ls","-l",NULL);
        //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("after exec!\n");  //无效,不会打印。
    }
    
    return 0;
}

结论:
1. 以上的6个函数功能都是类似的,只需要记忆其中一个即可。
2. exec函数族替换掉一个子进程,所以在exec函数族函数后面的代码,都无效。

     
二、如何确保子进程先运行?   --> 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、确保子进程先运行。
2、必须要在子进程中调用exit()/exec函数族其中的一个函数接口才能解锁父进程。
3、如果子进程没有exit()/exec函数族其中的一个函数接口就会导致程序奔溃。


    例子1: 使用exit函数来激活父进程。

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

int main(int argc,char *argv[])
{
    pid_t x;
    x = vfork();
    if(x > 0)
    {
        printf("parent.....\n");
    }
    
    if(x == 0)
    {
        sleep(2);
        printf("child....\n");
        exit(0);
    }
    
    return 0;
}


    例子2: 使用exec函数族函数来激活。

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

int main(int argc,char *argv[])
{
    pid_t x;
    x = vfork();
    if(x > 0)
    {
        printf("parent.....\n");
    }
    
    if(x == 0)
    {
        sleep(2);
        printf("child....\n");
        execl("/bin/ls","ls","-l",NULL);
        printf("xxx..\n");
    }
    
    return 0;
}

    例子3: 子进程不调用exit()/exec函数族来激活父进程。

vfork: cxa_atexit.c:100: __new_exitfn: Assertion `l != NULL' failed.
Aborted (core dumped)


三、研究父子进程的资源问题。
1、研究vfork()资源问题。

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

int main(int argc,char *argv[])
{
    int a = 0;  //vfork之前的资源
    
    pid_t x;
    x = vfork();
    if(x > 0)
    {
        int b = 10;
        a++;
        printf("parent a = %d\n",a); //2
        //printf("parent c = %d\n",c);  //编译出错
        printf("parent b = %d\n",b); //10
        exit(0);
    }
    
    if(x == 0)
    {
        int c = 20;
        sleep(2);
        a++;
        printf("child a = %d\n",a);  //1
        printf("child c = %d\n",c);  //20
        //printf("child b = %d\n",b);   //编译出错
        exit(0);
    }
        
    return 0;
}

结论:
1. vfork之前的资源,父子进程都是共享的。
2. vfork之后的资源,父子进程都是独立的。


2、研究fork()父子进程的资源问题。

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

int main(int argc,char *argv[])
{
    int a = 0;  //fork之前的资源
    
    pid_t x;
    x = fork();
    if(x > 0)
    {
        int b = 10;
        sleep(2);
        a++;
        printf("parent a = %d\n",a); //1
        printf("parent b = %d\n",b); //10
        //printf("child c = %d\n",c);
    }
    
    if(x == 0)
    {
        int c = 20;
        a++;
        printf("child a = %d\n",a); //1
        //printf("child b = %d\n",b);
        printf("child c = %d\n",c);
        exit(0);
    }
    
    return 0;
}

结论:
1. fork()之前的资源,父进程会拷贝一份一模一样的给子进程。
2. fork()之后的资源,在父进程中定义的,只能在父进程中使用,在子进程中定义的,只能在子进程中使用。


------------------------------------------------------------------


int main(int argc,char *argv[])
{
    //1. 现在还是只有一个父亲,结果父亲中奖了,中了100万。
    int a = 100; //父亲的财产
    
    //2. 这时候,父亲带着这100万,去生了一个小孩。
    pid_t x;
    x = fork();
    if(x > 0)
    {
        //3. 父亲查看自己的财产
        printf("parent a = %d\n",a); //100
        
        //6. 父亲看到自己有这么多钱,决定去澳门赌一赌,结果输了50万
        a -= 50;
        
        //7. 父亲赌完之后,赶紧查看自己的财产
        printf("parent a = %d\n",a); //50
        
        //9. 父亲利用自己剩余50万,给贷款买了一套房,200平
        int b = 200;
        
        //11. 看到你进不来,我就放心了,我来进去住一下
        printf("parent b = %d\n",b); //200  舒服啊!
    }
    
    if(x == 0)
    {
        //政府有补贴,生小孩之前,父亲有多少财产,政府就会补贴一模一样的给小孩。
        //4. 孩子出生后,也查看自己的财产。
        printf("child a = %d\n",a); //100
        
        //5. 小孩查看完之后,决定去睡了觉。
        sleep(3);
        
        //8. 小孩醒了之后,听说父亲去赌钱输了,赶紧查看自己的财产。
        printf("child a = %d\n",a); //100  心里想着,父亲输钱了,关我什么事。
        
        //10. 据说父亲买房了,我想去看看多大的
        printf("parent b = %d\n",b); //父亲说,这是我的房子,关你什么事,还要把你告上法庭(编译出错)
    }
    
    return 0;
}
--------------------------------------------------------------


四、进程之间的通信。
1、为什么要学习进程之间的通信?
在linux下,任意两个进程之间资源都是独立,进程A的数据,进程B不可能访问到的,进程B的数据,进程A不可能访问到的,也就是说,所有的进程之间,哪怕是父子进程,资源都是独立分开的,所以我们必须要学习进程之间的通信,让两个进程之间进行数据交换。

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

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

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

4)共享内存。
多个进程使用同一块内存空间。

五、进程之间的通信方式   ---  无名管道。
1、什么是无名管道?作用机制如何?
无名管道只能作用于亲缘关系的进程之间,不能作用于陌生的两个进程之间。
其实无名管道就是一个数组,这个数组两端分别是写端和读端,进程想写入数据,则往写端写入,进程想读取数据,则读取读端上的数据。

2、实现步骤。
1)申请数组  ->  int fd[2]
   现在里面的数据并不是读端与写端。

2)使用函数对数组进行初始化。   -->  pipe()   --> man 2 pipe
功能: create pipe
    //创建一个管道

使用格式:
    #include <unistd.h>

       int pipe(int pipefd[2]);

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

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

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


      例题1: 初始化成功的读端与写端分别是多少?

#include <unistd.h>
#include <stdio.h>

int main(int argc,char *argv[])
{
    int fd[2] = {0};
    printf("fd[0] = %d\n",fd[0]);
    printf("fd[1] = %d\n",fd[1]);
    
    pipe(fd);
    
    printf("fd[0] = %d\n",fd[0]);  //3  读端
    printf("fd[1] = %d\n",fd[1]);  //4  写端
    
    return 0;
}
    

     例题2: 使用无名管道,让父子进程之间进行通信。(子进程发送一个hello给父进程)

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

int main(int argc,char *argv[])
{
    //1. 初始化管道
    int fd[2] = {0};
    pipe(fd);
    
    //2. 带着这条管道,去产生一个子进程
    pid_t x;
    x = fork();
    
    if(x > 0)
    {
        char buf[10] = {0};
        read(fd[0],buf,sizeof(buf));
        printf("from child:%s\n",buf);
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        char buf[10] = "hello";
        write(fd[1],buf,strlen(buf));
        exit(0);
    }
    
    return 0;
}


    练习1: 实现父进程给子进程发送数据。


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

int main(int argc,char *argv[])
{
    //1. 初始化管道
    int fd[2] = {0};
    pipe(fd);
    
    //2. 带着这条管道,去产生一个子进程
    pid_t x;
    x = fork();
    
    if(x > 0)
    {
        char buf[10] = "hello";
        write(fd[1],buf,strlen(buf));
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        sleep(3);
        char buf[10] = {0};
        read(fd[0],buf,sizeof(buf));
        printf("from parent:%s\n",buf);
        exit(0);
    }
    
    return 0;
}

六、进程之间通信方式  --  有名管道
1、什么是有名管道?作用机制如何?
无名管道是一个数组来的,而有名管道是一个文件来的,因为在linux下,任意两个进程都可以看到这个文件,所以有名管道作用范围是整个linux系统下任意两个进程。

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/kkk"
    mode:管道文件的权限                0777

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

  
    例题3: 尝试在家目录下创建一个管道文件,名字叫test_fifo

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>

int main(int argc,char *argv[])
{
    int ret;
    ret = mkfifo("/home/gec/test_fifo",0777);  ///注意你的ubuntu有没有/home/gec这个路径
    if(ret == 0)
    {
        printf("mkfifo success!\n");
    }
    else{
        printf("mkfifo error!\n");
    }
    
    return 0;
}


    例题4: 使用有名管道,实现任意两个进程之间通信。

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

int main(int argc,char *argv[])
{
    //1. 创建管道文件
    int ret;
    ret = mkfifo("/home/gec/test_fifo",0777);
    if(ret < 0)
    {
        printf("mkfifo error!\n");
    }
    
    //2. 打开文件
    int fd = open("/home/gec/test_fifo",O_RDWR);
    if(fd < 0)
    {
        printf("open error!\n");
    }
    
    //3. 不断读取管道文件上的数据
    char buf[100];
    while(1)
    {
        bzero(buf,sizeof(buf));
        read(fd,buf,sizeof(buf));
        printf("from fifo:%s",buf);
        
        if(strncmp(buf,"quit",4) == 0)
        {
            break;
        }
    }
    
    //4. 关闭文件
    close(fd);
    
    return 0;
}

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

int main(int argc,char *argv[])
{
    //1. 创建管道文件
    int ret;
    ret = mkfifo("/home/gec/test_fifo",0777);
    if(ret < 0)
    {
        printf("mkfifo error!\n");
    }
    
    //2. 打开管道文件
    int fd = open("/home/gec/test_fifo",O_RDWR);
    if(fd < 0)
    {
        printf("open error!\n");
    }
    
    //3. 不断发送数据到管道文件中
    char buf[100];
    while(1)
    {
        bzero(buf,sizeof(buf));
        
        fgets(buf,sizeof(buf),stdin);
        
        write(fd,buf,strlen(buf));
        
        if(strncmp(buf,"quit",4) == 0)
        {
            break;
        }
    }
    
    //4. 关闭文件
    close(fd);
}

执行结果:
可以正常通信,但是由于管道文件存在,所以创建失败。

解决方案:
1. 可以利用open函数来打开文件,然后判断该文件是否存在。
2. 利用access函数来判断文件是否存在。


七、进程之间的通信方式   ---  信号
1、在linux下,有哪些信号?
gec@ubuntu:~$ kill -l
 1) SIGHUP     2) SIGINT     3) SIGQUIT     4) SIGILL     5) SIGTRAP
 6) SIGABRT     7) SIGBUS     8) SIGFPE     9) SIGKILL    10) SIGUSR1
11) SIGSEGV    12) SIGUSR2    13) SIGPIPE    14) SIGALRM    15) SIGTERM
16) SIGSTKFLT    17) SIGCHLD    18) SIGCONT    19) SIGSTOP    20) SIGTSTP

例如:
19) SIGSTOP

19       --> 信号值
SIGSTOP  --> 信号名字

其实信号值与信号名字是等价的,因为它们是一个宏定义来的,是被定义在一个头文件中:
路径:/usr/include/asm-generic/signal.h

#define SIGHUP         1
#define SIGINT         2
#define SIGQUIT         3
#define SIGILL         4
#define SIGTRAP         5

2、常用信号?
2) SIGINT    -->  等价于 ctrl + C
9) SIGKILL   -->  终止进程
10) SIGUSR1
12) SIGUSR2  -->  提供给用户使用的两个信号,没有什么特殊的作用
18) SIGCONT  -->  继续信号
19) SIGSTOP  -->  暂停信号
  
3、在linux下,这些信号由谁来发出?
1)由系统来发出。
14) SIGALRM     --> 当在程序中调用alarm()时,如果到点了,就会自动发出这个信号
17) SIGCHLD     --> 当子进程退出时,自动发出这个信号给父进程。

2)信号由用户来发出。
如果是用户来发送,则需要学习kill/killall这两个命令。

4、用户怎么样才能主动发送信号?

---------------------------方法一----------------------
直接通过kill命令给进程的PID号发送。

1)首先查看目标进程的PID号。  -->  ps -ef

      PID号
gec       10975   2774 84 16:33 pts/18   00:00:10 ./ggy

2)通过kill命令发送9号信号给进程。
gec@ubuntu:~$ kill -9 10975
gec@ubuntu:~$ kill -SIGKILL 10975


---------------------------方法二----------------------
直接通过killall命令给进程的名字发送。
gec@ubuntu:~$ killall -9 ggy
gec@ubuntu:~$ killall -SIGKILL ggy    --> 只要进程名字叫ggy,都会收到这个信号


八、关于信号的函数接口。
1、在程序中,信号收发原理是怎么样?
首先让进程去捕捉一个信号(当收到某一个信号时,我需要做什么事情),将来这个进程真的收到这个信号后,就会马上去做这件事情。

2、如何发送信号给另外一个进程?   --> kill()   --> man 2 kill
功能:kill - send signal to a process
        //发送一个信号给进程

使用格式:
    #include <sys/types.h>
        #include <signal.h>

       int kill(pid_t pid, int sig);

参数:
    pid: 目标进程PID号
    sig: 想发送的信号值

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

3、如何在程序中捕捉信号?   -->  signal()   --> man 2 signal
功能:signal - ANSI C signal handling
    //设置信号的处理函数

使用格式:
    #include <signal.h>

       typedef void (*sighandler_t)(int);   //   sighandler_t  等价于   void (*)(int)

       sighandler_t signal(int signum, sighandler_t handler);

参数:
    signum:需要捕捉的信号
    handler:信号处理函数  -> 必须长: void func(int a)   --> 设置一下你收到信号之后,要做什么事情。

返回值:
    成功:信号处理函数的值(第二个参数)
    失败:SIG_ERR


void func(int sig)
{
    printf("好!\n");
}

signal("请你吃饭",func);  //以后只要收到"请你吃饭",就会马上执行func这个函数。

    例题: 子进程发送一个SIGUSR1信号给父进程,父进程收到这个信号之后,就把这个信号值打印出来。

#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <strings.h>
#include <sys/wait.h>
#include <stdlib.h>

void func(int sig)  //sig就是捕捉的信号值
{
    printf("catch sig = %d\n",sig);
}

int main(int argc,char *argv[])
{
    //1. 创建一个子进程
    pid_t x;
    x = fork();
    
    if(x > 0)
    {
        //2. 让父进程先捕捉SIGUSR1,只要收到这个信号,就打印该信号值。
        signal(SIGUSR1,func);
        
        wait(NULL);
        
        exit(0);
    }
    
    if(x == 0)
    {
        sleep(5);
        
        kill(getppid(),SIGUSR1);
        
        exit(0);
    }    
}

4、挂起进程,直到收到一个信号为止。  -->  pause()   --> man 2 pause
功能: pause - wait for signal
        //等待信号的到达

使用格式:
    #include <unistd.h>

       int pause(void);

参数:无
返回值:
    只要收到信号的时候才会返回。


----------------------------------------------------------
#include <signal.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <strings.h>
#include <sys/wait.h>
#include <stdlib.h>

void func(int sig)  //sig就是捕捉的信号值
{
    printf("catch sig = %d\n",sig);
}

int main(int argc,char *argv[])
{
    //1. 创建一个子进程
    pid_t x;
    x = fork();
    
    if(x > 0)
    {
        sleep(5);
        
        kill(x,SIGUSR1);
        
        wait(NULL);
        exit(0);
    }
    
    if(x == 0)
    {
        //2. 让父进程先捕捉SIGUSR1,只要收到这个信号,就打印该信号值。
        signal(SIGUSR1,func);
        
        //3. 原地等待信号到达
        pause();
        
        exit(0);    
    }    
}
----------------------------------------------------------

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

肖爱Kun

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值