02-系统编程(exec函数与管道)

02-exec函数簇与管道




一、从内存角度分析父子进程资源问题

两个陌生的进程之间的资源不共用,在进程1中声明的变量,是不可以在进程2中使用。
但是父子进程是有父子关系的,那么它们对于资源有什么特例?

代码参考:

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

int main()
{
    //原本进程的代码
    int a = 100;
    printf("main\n");
    
    //创建一个子进程
    pid_t id = fork();
    if(id == -1)//出错
    {
        printf("fork error\n");
        return -1;
    }
    else if(id >0)//父进程
    {
        a = 250; //父进程修改变量a的值
        printf("parent:%d a addr:%p value:%d\n",getpid(),&a,a);     
    }
    else if(id == 0)//子进程
    {
        sleep(1);

        printf("child:%d a addr:%p value:%d\n",getpid(),&a,a);
        exit(0);//让子进程到这里就结束
    }
    
    printf("111\n");
    //阻塞等待子进程退出
    wait(NULL);
    
    return 0;
}

运行结果:
在这里插入图片描述
结果:父子进程a的地址都是一样,但是父进程中的变量a 和 子进程中的变量a 互不影响。
结论:
1)父进程在fork()时,会将父进程的空间拷贝一份给子进程。
2)父进程与子进程 拥有独立的内存空间,互不影响。

二、产生子进程的另一个函数vfork

#include <sys/types.h>
#include <unistd.h>
pid_t vfork(void);
函数作用:
创建一个子进程
特点:
1、子进程与父进程共享内存空间,更加准确来说,子进程在调用exec函数簇或者exit函数之前内存空间是共享的
2、一定是子进程先运行,而且是等子进程结束之后,父进程才开始运行
3、当子进程调用exit之后,父进程才会往下执行
4、你在引用的时候,最好尽快结束子进程(注意)
5、使用vfork 创建出来的子进程 ,一定要使用exec或者exit退出进程。否则导致 程序死锁,程序会出现异常

代码参考:

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

//int a = 100;

int main()
{
    //原本进程的代码(主要是用来验证栈、数据段、堆里面的数据是共享的)
    //int a = 100; //a是局部变量,是栈区

    static int a = 100; //a是静态变量,存放在数据段

    printf("main\n");
    
    //使用vfork创建一个子进程,父进程与子进程共享数据段
    pid_t id = vfork();
    if(id == -1)//出错
    {
        printf("fork error\n");
        return -1;
    }
    else if(id >0)//父进程
    {   
        printf("parent:%d a addr:%p value:%d\n",getpid(),&a,a);     
    }
    else if(id == 0)//子进程
    {       
        a = 250; //子进程修改变量a的值
        printf("child:%d a addr:%p value:%d\n",getpid(),&a,a);
        sleep(5);
        exit(0);//让子进程到这里就结束
    }
    
    printf("111\n");
    //阻塞等待 子进程退出
    wait(NULL);
    
    return 0;
}

运行结果:
在这里插入图片描述
小练习:验证除了栈是共享的,数据段或者堆里面是否也共享

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

// int a = 100;

int main()
{
    // 原本进程的代码(主要是用来验证栈、数据段、堆里面的数据是共享的)
    // int a = 100; //a是局部变量,是栈区
    // static int a = 100; //a是静态变量,存放在数据段
    int *p = malloc(4); // 堆空间
    *p = 250;

    printf("main\n");

    // 使用vfork创建一个子进程,父进程与子进程共享数据段
    pid_t id = vfork();
    if (id == -1) // 出错
    {
        printf("fork error\n");
        return -1;
    }
    else if (id > 0) // 父进程
    {
        // printf("parent:%d a addr:%p value:%d\n",getpid(),&a,a); //250
        printf("parent:%d a addr:%p value:%d\n", getpid(), p, *p); // 250
    }
    else if (id == 0) // 子进程 (子进程里面的工作尽量不要太多)
    {
        // a = 250; // 子进程修改变量a的值
        // printf("child:%d a addr:%p value:%d\n",getpid(),&a,a); //250
        printf("child:%d a addr:%p value:%d\n", getpid(), p, *p); // 250
        sleep(5);
        exit(0); // 让子进程到这里就结束
    }

    printf("111\n");
    // 阻塞等待 子进程退出
    wait(NULL);

    return 0;
}

三、exec函数簇(功能类似于system)

#include <unistd.h>
extern char **environ;

int execl(const char *pathname, const char arg, … / (char *) NULL */);
int execv(const char *pathname, char *const argv[]); //vector

int execlp(const char *file, const char arg, …/ (char *) NULL */);
int execle(const char *pathname, const char arg, … /, (char *) NULL, char *const envp[] */);

int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
函数作用
在进程中加载新的程序,覆盖原有的代码,重新运行
参数
pathname------》创建出子进程之后,你要让子进程执行哪个程序,将那个程序的路径+名字传递进来
比如创建出子进程之后,让他去执行另外一个程序 “./hello”
file --------》创建出子进程之后,你要让子进程执行哪个程序,将那个程序的名字传递进来(其实也可以传递路径+名字)
arg-------》执行程序时,需要的参数列表,以自身程序名字为开始,NULL作为结束标志
比如,你要执行 ./hello --》传递参数 123 —> “hello” 123 NULL
注意:只要进程 被exec族替换掉之后,在exec函数之后的代码都不会执行了
记忆方法
l->列表 v->argv p->指定路径 e->自定义路径

第一种方法:
execl(“./a.out”, “a.out”, “abcd”, NULL);

第二种方法:
char *const argv[ ] = {“hello”,“1.c”,“2.c”,NULL};
execv(“./hello”, argv);

第三种方法:

execvp()会从PATH环境变量所指的目录中查找符合参数 file 的文件名, 找到后便执行该文件, 然后将第二个参数argv传给该欲执行的文件.
*/
char *const argv[ ] = {“ls”,“-l”,“2.c”,NULL};
int ret = execvp(“ls”, argv);
if(ret == -1){
perror(“execvp error”);
}

第四种方法:
//execvpe 不仅仅会去PATH环境变量路径下寻找程序,还会去指定的路径envp下寻找
char *const argv[ ] = {“hello”,“1.c”,“2.c”,NULL};
char *const envp[ ] = {“./”};
execvpe(“./hello”,argv,envp);//也可以不加./直接execvpe(“hello”,argv1,envp);

说明:execvpe这一个函数有个小bug会警告(是由编译版本产生)

小练习:写一个函数,编译成hello可执行文件要求效果如下(使用带参数的main函数)将四种方法都尝试一遍
./hello 123 345 567 (参数的个数不定)
argv[1] = 123
argv[2] = 345
argv[3] = 567

例子:

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

//int a = 100;

int main()
{
    printf("main\n");
    
    pid_t id = fork();
    if(id == -1)//出错
    {
        printf("fork error\n");
        return -1;
    }
    else if(id >0)//父进程
    {
        printf("父进程 ID:%d\n",getpid());
    }
    else if(id == 0)//子进程
    {
        printf("子进程 ID:%d\n",getpid());
        //子进程去执行别的进程(应用程序) ./hello 
        #if 0
        execl("./hello","hello","hai",NULL);
        #elif 0
        char *const argv[ ] = {"hello","1.c","2.c",NULL};
        execv("./hello", argv);
        #elif 0
        /*
        execvp()会从 PATH 环境变量所指的目录中查找符合参数 file 的文件名, 找到后便执行该文件, 然
        后将第二个参数 argv 传给该欲执行的文件.
        */ 
        char *const argv[ ] = {"ls","-l","2.c",NULL};
        int ret = execvp("ls", argv);
        if(ret == -1){
            perror("execvp error");
        }

        #elif 1
        //execvpe 不仅仅会去PATH环境变量路径下寻找程序,还会去指定的路径envp下寻找
        char *const argv[ ] = {"hello","1.c","2.c",NULL};
        char *const envp[ ] = {"./"};
        execvpe("hello",argv,envp);

        #endif

        //execl后面的代码都不会执行了
        printf("chlid end\n");  
        exit(0);
    }
    
    printf("parent end\n");
    //阻塞等待 子进程退出
    wait(NULL);
    
    return 0;
}

小练习:写一个程序,这个程序创建一个子进程后,子进程打印haha,然后监控键盘,通过键盘的参数去启动另外一个程序,
比如键盘输入 hello hai nihao ,就代表启动 hello程序,传递 hai nihao 到程序hello中,并且在程序hello把传递过来的参数打印出来

代码参考:

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

#if 0

#endif

int main()
{
    printf("main id:%d\n",getpid());
    
    pid_t id = fork();
    if(id >0) //父进程
    {
        printf("父进程 ID:%d\n",getpid());
    }
    else if(id ==0) //子进程
    {
        //创建子进程之后,让子进程 去执行别的程序
        printf("子进程 ID:%d \n",getpid());
        char input_str[10][256] = {0}; //char (*p)[256] = input_str;  //char (*)[256]
        char *argv_buf[10] = {NULL}; //数组 ,每个元素存储的是 地址 char*
        // char*(*p)  = argv;  // char **

        //input_str[0]--->"hello"  
        //input_str[1]--->123
        //input_str[2]--->456
        //input_str[3]--->789

        int cnt = 0;

        //1、用户输入 子进程要执行的新程序的名字  以及 要传递 新程序的参数
        printf("请输入你要让子进程执行的程序名字 以及参数:");  //  hello  123  456  789  end
        while(1)
        {
            scanf("%s",input_str[cnt]);
            //如果输入的是 end ,则退出
            if(0 == strcmp(input_str[cnt],"end"))
            {
                break;
            }
            //argv[0] ---》char * 存储字符串
            argv_buf[cnt] = input_str[cnt];
            cnt++;
        }

        char fileName[1024] = {0};
        sprintf(fileName,"%s%s","./",input_str[0]);
        //2、调用exec函数簇 执行 新的程序 
        //execv(const char *path, char**argv);
        if(execv(fileName,argv_buf) == -1)
        {  //char (*)[256]
        
            perror("execv error");
        }
    }

    wait(NULL);

    return 0;
}

四、进程之间通信方式(重点)

1.为什么要实现进程之间的通信

例如:
./project -> 开启了一个名字叫project的进程。
./test -> 开启了一个名字叫test的进程。
通过学习进程之间的通信,使得不同的进程之间能够实现数据的交换,例如test进程发送数据给project进程,project进程收到数据之后,根据数据做出相应的事情。 (test进程控制project进程)

2.在linux下,进程之间通信方式有哪些,都有什么特点

以下几种进程之间的通信有一个共通的特点,都是只能在同一台主机内部的进程使用。

1)管道通信。–>pipe
管道通信分为有名管道与无名管道,管道是一个特殊的文件,进程通过将数据写入到管道中,另外一个进程从管道中读取数据出来。
2)信号->signal
在linux下,有非常多的信号,例如:暂停,继续,停止…,某一个进程通过发送信号给另外一个进程,从而控制另外一个进程的运行状态。
3)消息队列 -->message
某一个进程把消息发送到队列上,另外一个进程就可以读取队列上的数据,消息队列好处:进程可以读取队列上某一个特定的数据。
4)共享内存 -->share mem
多个进程访问同一片内存空间。
5)信号量(理论难点)

五、进程之间通信方式之一 无名管道

1.什么是无名管道?作用机制如何?

无名管道只能作用于亲缘关系的进程之间的通信,例如父子进程。无名管道就是一个没有名字的管道文件,相当于一个队列结构,
fd[1]为写入端(入队),fd[0]为读出端(出队)。其中信息读出后即删除,再次读取时即为下一个信息。

2.创建一个无名管道文件

#include <unistd.h>
int pipe(int pipefd[2]); -> 执行这个函数之后,得到两个文件描述符 // int *pipefd
函数作用:
创建一个无名管道文件
参数:
pipefd 一个具有2个int类型变量的数组。(2个文件描述符)
返回值:
成功:0
失败:-1

注意:

1、pipefd[0] -> 读端 pipefd[1] -> 写端
2、没有名字,因此无法使用 open( )。
3、只能用于亲缘进程间(比如父子进程、兄弟进程、祖孙进程……)通信,因为他只能在一个进程中被创建出来,然后通过继承的方式将他的文件描述符传递给子进程
4、半双工(单方向)工作方式:读写端分开。

举例1:初始化的读端写端的文件描述符是多少?

代码参考:

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

int main()
{
    // 创建无名管道(不需要open)
    int fd1 = open("hello.c", O_RDWR);
    close(fd1);

    int fd[2];
    pipe(fd);

    
    printf("fd[1]=%d fd[0]=%d\n", fd[1], fd[0]);

    close(fd[0]);
    close(fd[1]);
    
    return 0;
}

说明:无名管道文件描述符的值,是从1024个值里面按照顺序拿取

举例2:实现使用无名管道,让父子进程通信

代码参考:

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

int main()
{
    int fd[2];
    //创建一个无名管道文件 ,创建成功之后 fd[0] 读端  fd[1] 写端
    pipe(fd); //注意,不可以使用open打开无名管道,因为它没有名字,创建的时候默认已经打开了
    
    //创建一个子进程 ,实现父进程与子进程的通信
    pid_t id = fork();
    if(id == -1)//出错
    {
        printf("fork error\n");
        return -1;
    }
    else if(id >0)//父进程
    {
        //给子进程发送数据,也就是说将数据写入 fd[1] 管道中
        printf("[%d]父进程 给子进程发送数据:",getpid());           
        char sendbuf[1024] ={0};
        scanf("%s",sendbuf);
        write(fd[1],sendbuf,strlen(sendbuf));
    }
    else if(id == 0)//子进程
    {
        sleep(1);
        //接收父进程的信息,也就是说从 读端 管道中 读取数据 fd[0]
        char recvbuf[1024]={0};
        read(fd[0],recvbuf,sizeof(recvbuf));        
        printf("[%d]子进程 接收父进程数据:%s\n",getpid(),recvbuf);            
    }

    int status; 
    waitpid(-1,&status,WCONTINUED);//阻塞等待指定进程的退出(如果是-1表示阻塞等待子进程)

    return 0;
}

运行结果:
在这里插入图片描述
小练习: 父进程可以从键盘上输入数据,发送给子进程,当父进程发送 "exit"时,子进程结束,并且父进程等待子进程结束之后再退出

代码参考:

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

int main()
{
    // 创建无名管道
    int fd[2];
    pipe(fd);

    // ret作为返回值用
    int ret = 0;

    pid_t id = fork();
    if (id == -1)
    {
        perror("fork fail");
        return -1;
    }
    else if (id > 0) // 父进程
    {
        // 写管道fd[1];
        char buf[1024] = {0};
        while (1)
        {
            memset(buf, 0, sizeof(buf)); // 10000
            scanf("%s", buf);
            ret = write(fd[1], buf, strlen(buf));
            printf("[%d]write ret=%d\n", getpid(), ret); //调试代码一定要有
            if (strcmp(buf, "exit") == 0)
                break;
        }

        // 父进程阻塞等待
        wait(NULL);
        close(fd[1]); //关闭文件描述符
    }
    else if (id == 0) // 子进程
    {
        // 读管道fd[0]
        char buf[1024] = {0}; // 5000

        while (1)
        {
            memset(buf, 0, sizeof(buf)); // 10000
            ret = read(fd[0], buf, sizeof(buf));
            printf("[%d]read:%s ret:%d\n", getpid(), buf, ret); 调试代码一定要有

            if (strcmp(buf, "exit") == 0)
            {
                close(fd[0]); //关闭文件描述符
                exit(0); //子进程退出
            }
        }
    }

    return 0;
}

运行结果:
在这里插入图片描述

六、进程之间通信之一 有名管道

1.什么是有名管道?机制如何?

有名管道文件就是一个有名字的管道文件。在linux下,所有的进程都是可以看到这个文件,
所以有名管道作用范围是整个linux系统下任意的两个进程。

2.如何创建有名管道? -> mkfifo() //先进先出

#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
函数作用:
创建一个有名管道文件
参数:
pathname有名管道文件的路径+名字。 例如: /home/gec/fifo_test(有名管道不要在共享目录里面)
mode管道文件的权限0777
返回值:
成功:0
失败:-1

举例1:尝试在家目录下创建一个有名管道,名字为fifo1

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

#define  FIFO_FILE  "/home/gec/I'm the handsomest"

int main()
{
        //先判断文件是否存在,如果存在则不用创建了
        if(access(FIFO_FILE, F_OK) == -1)//access 判断文件是否存在,如果不存在则返回 -1
        {
            //所以是不存在的时候才进来创建              
            if(mkfifo(FIFO_FILE,0777) == -1)//创建一个有名管道文件 
            {
                perror("mkfifo error");
                return -1;
            }
        }

    return 0;
}

运行结果:
在这里插入图片描述
举例2:有名管道文件实现两个进程的通信

代码参考:

写段(发送)

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

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

#define FIFO_FILE1 "/home/gec/I'm the handsomest"

/*
    单进程1
*/
int main()
{
    // 创建有名管道FIFO_FILE1
    if (access(FIFO_FILE1, F_OK) == -1)
    {
        if (mkfifo(FIFO_FILE1, 0777) == -1)
        {
            printf("mkfifo %s fail\n", FIFO_FILE1);
            return -1;
        }
    }

    // 获取有名管道的文件描述符
    int fd = open(FIFO_FILE1, O_RDWR);
    if (fd < 0)
    {
        printf("open %s fail\n", FIFO_FILE1);
        return -1;
    }

    int ret;
    char buf[1024] = {0};
    while (1)
    {
        bzero(buf,sizeof(buf));
        scanf("%s",buf);
        // 通过有名管道写数据
        ret = write(fd, buf, strlen(buf));
        printf("write ret:%d\n", ret);

        if(strcmp(buf,"byebye") == 0)
            break;
    }

    // 关闭有名管道
    close(fd);

    return 0;
}

读段(接收)

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

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

#define FIFO_FILE1 "/home/gec/I'm the handsomest"

/*
    单进程2
*/
int main()
{
    // 创建有名管道FIFO_FILE1
    if (access(FIFO_FILE1, F_OK) == -1)
    {
        if (mkfifo(FIFO_FILE1, 0777) == -1)
        {
            printf("mkfifo %s fail\n", FIFO_FILE1);
            return -1;
        }
    }

    // 获取有名管道的文件描述符
    int fd = open(FIFO_FILE1, O_RDWR);
    if (fd < 0)
    {
        printf("open %s fail\n", FIFO_FILE1);
        return -1;
    }

    int ret;

    // 通过有名管道读数据
    char buf[1024] = {0};
    while (1)
    {
        bzero(buf, sizeof(buf));
        ret = read(fd, buf, sizeof(buf));
        printf("read buf:%s ret:%d\n", buf, ret);
        if (strcmp(buf, "byebye") == 0)
            break;
    }

    // 关闭有名管道
    close(fd);

    return 0;
}

运行结果:
在这里插入图片描述
注意:
有名管道一旦没有读者或者写者,系统会判定管道处于空闲状态(读和写同时存在),回释放管道里面的所有数据
总结:
1.管道是创建在内存中,进程结束空间释放,管道不复存在。
2.无名管道和有名管道都是半双工通信(单方向),实现双向通信需要建立两个管道。
3.无名管道是linux特殊文件。
4.无名管道只用于父子进程之间,有名管道可用于任意两个进程之间
5.用有名管道读取的时候,里面有什么就读什么,进程无法决定自己想读什么就读什么。(消息队列可以决定自己想读取的内容)

拓展

使用vfork 创建出来的子进程 ,一定要使用exec或者exit退出进程。否则导致 程序死锁,程序会出现异常。
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值