一、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;
}