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退出进程。否则导致 程序死锁,程序会出现异常。