进程和程序(二)
IPC(进程间通信)概念
IPC:InterProcess Communication,通过内核提供的缓冲区进行数据交换的机制。
IPC通信的方式有几种呢?
1、pipe 管道–最简单
只支持有血缘关系的进程进行通信。
2、fifo 有名管道
3、文件映射共享IO – 速度最快
4、本地socket – 最稳定
5、信号 – 携带信息量最小
6、共享内存
7、消息队列
这里主要介绍前三种。
管道
pipe函数
参数pipfd[2]为两个文件描述符。其中,pipfd[0]代表读,pipfd[1]代表写。
定义两个成员的pipfd数组,然后传入pipe函数,pipe函数会进行初始化,初始化完成之后就相当于在内核中开辟了缓冲区。
管道被称为半双工通信。
父子进程间通信
1、子进程写,父进程读
#include <stdio.h>
#include <unistd.h>
#include <string.h>
int main(int argc,char* argv[])
{
int fd[2];
pipe(fd);
//已建立好管道,fd[0]为读端,fd[1]为写端
pid_t pid = fork();
if(pid == 0)
{
//son
write(fd[1],"hello",5);
}
else if(pid > 0)
{
int ret;
//parent
char buf[10];
memset(buf,0,sizeof(buf));
ret = read(fd[0],buf,sizeof(buf));
if(ret>0)
{
write(STDOUT_FILENO,buf,ret);
}
}
return 0;
}
2、利用父子进程的pipe通信,实现ps aux|grep bash功能
“|”是管道命令操作符
“|”是管道命令操作符,简称管道符。利用Linux所提供的管道符“|”将两个命令隔开,管道符左边命令的输出就会作为管道符右边命令的输入。连续使用管道意味着第一个命令的输出会作为 第二个命令的输入,第二个命令的输出又会作为第三个命令的输入,依此类推。
它仅能处理经由前面一个指令传出的正确输出信息,也就是 standard output 的信息,对于 standard error 信息没有直接处理能力。
用法示例:
ls -l | more
该命令的含义是列出当前目录中的文档信息,并把输出送给more命令作为输入,more命令分页显示文件列表。
利用父子进程的pipe通信,实现ps aux|grep bash功能
把"|"比作管道,将ps aux命令的输出写入管道,然后grep bash命令去读取管道,并把结果显示出来。
这里让子进程负责ps aux,然后将输出写入管道。
让父进程负责grep bash,然后去读取管道。
已知,ps aux会将结果输出到标准输出,而现在想要将结果写到管道,因此,需要进行输出重映射。同理,grep接收(读取)时是也是默认从标准输入中读取的,因此,也需要进行输入重映射。实现重映射使用dup2函数。
我们画图来分析:
#include <stdio.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//son
//son实现ps aux
//需要将标准输出重定向到管道的写端
dup2(fd[1],STDOUT_FILENO);//注意dup2的参数顺序
execlp("ps","ps","aux",NULL);
}
else if(pid > 0)
{
//parent
//parent实现grep bash
//需要将标准输入重定向到管道的读端
dup2(fd[0],STDIN_FILENO);
execlp("grep","grep","bash",NULL);
}
return 0;
}
一般情况下,会在父进程中回收子进程,这里没有实现。但等父进程运行完毕退出,僵尸进程也就没有了。
运行程序:
注意:上图中的光标处,说明grep bash还在运行。
另开一个终端,然后使用ps aux|grep bash命令来验证下。
结果一致。
再使用ps aux命令来查看下。
会发现僵尸进程,因为我们在父进程还在运行,父进程也没有回收子进程。等父进程运行完毕退出,僵尸进程也就没有了。
改进代码,让grep完成后就退出。
grep还需要进一步来理解。
grep:过滤内容。
grep “hello” [文件名]
含义是:在指定文件中查找带"hello"的内容,并显示出来。
如果不带文件名,比如执行
grep “hello”
就会堵塞住,然后等待标准输入,当匹配到输入的带有指定的字符串时,就会反馈出来。
要解决这个问题,就涉及到管道的特性。
从上图中,可以看到,父子进程都指向管道的读写两端。当子进程运行完毕退出后,就不再指向管道的读写两端了,但是父进程自己的还指向着管端的读写两端。
父进程认为还有写端没有关闭,还会有人(进程)在某个时刻会给它写入数据,所以会一直处在等待输入的状态。
因此,父进程只负责写,所以父进程应该在重映射之前把写端给关闭。
还有,子进程只负责读,所以子进程应该在重映射之前把读端给关闭。
管道的读写行为
示例一 父进程读管道,写端全部关闭。
#include <stdio.h>
#include <unistd.h>
int main(int argc,char* argv[])
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//son
//关闭读端
close(fd[0]);
//沉睡三秒
sleep(3);
//写数据
write(fd[1],"hello\n",6);
//写完关闭写端
close(fd[1]);
//让子进程陷入沉睡
while(1)
{
sleep(1);
}
}
else if(pid > 0)
{
//parent
//关闭写端
close(fd[1]);
char buf[10] = {0};
//循环read
while(1)
{
int ret = read(fd[0],buf,sizeof(buf));
if(ret == 0)
{
printf("read over.\n");
break;
}
if(ret > 0)
{
write(STDOUT_FILENO,buf,ret);
}
}
}
return 0;
}
运行结果:
示例二 子进程写管道,读端全部关闭。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main(int argc,char* argv[])
{
int fd[2];
pipe(fd);
pid_t pid = fork();
if(pid == 0)
{
//son
//关闭读端
close(fd[0]);
//沉睡三秒
sleep(3);
//写数据
write(fd[1],"hello\n",6);
//让子进程陷入沉睡
while(1)
{
sleep(1);
}
}
else if(pid > 0)
{
//parent
//关闭写端
close(fd[0]);
close(fd[1]);
//验证:读端全部关闭,产生一个信号SIGPIPE,
//子进程因异常而退出,父进程使用wait函数回
//收子进程,打印输出是否是被该信号杀死。
int status;
// 传入status的地址
pid_t wait_id = wait(&status);
printf("wait ok,wait_id = %d.\n",wait_id);
// 如果WIFEXITED(status)为真,说明是正常死亡
if(WIFEXITED(status))
{
printf("the reason of child die is %d.\n", WEXITSTATUS(status));
}
if(WIFSIGNALED(status))
{
printf("the reason of child die is %d signal.\n", WTERMSIG(status));
}
//让父进程陷入休眠
while(1)
{
sleep(1);
}
}
return 0;
}
使用kill -l命令来看看13号信号是什么
管道缓冲区大小
可以使用 ulimit -a命令来查看当前系统中创建管道文件所对应的内核缓冲区大小。通常为:8*512 byte。管道的大小和管道的容量还不一样,比如上面提到的管道已满,说的实际上是管道的容量,管道的容量比管道的大小要大一些。
对于缺点1的意思是:为了保证没有分叉路,通常只能由父进程写/读,子进程读/写。
FIFO
FIFO常被称为命名管道,以区分管道,管道只能用于有血缘关系的进程间,但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种(p,管道文件)。但FIFO文件在磁盘上没有数据块,仅仅用来标识内核中一条通道。各进程可以打开这个文件进行read/write,实际上是在读写内核通道,这样就实现了进程间通信。另外,使用统一fifo文件,可以有多个读端和多个写端。实际上就是对文件进行读写。
创建fifo文件的方式有两种,一种是使用命令,一种是使用函数。
mkfifo函数在第三章。
在终端中输入命令 man 3 mkfifo 可查看。
写两个程序来实现下(一个运行的程序就是一个进程)
先写个写的
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main(int argc,char *argv[])
{
if(argc!=2)
{
printf("%s filepath",argv[0]);
return -1;
}
//当前目录有一个fifo文件,否则还要创建
//以只写方式打开fifo文件
int fd = open(argv[1],O_WRONLY);
//写
char buf[256];
int num=1;
while(1)
{
memset(buf,0x00,sizeof(buf));
sprintf(buf,"hello world %04d\n",num++);
//如果用sizeof,就是256。
write(fd,buf,strlen(buf));
sleep(1);
}
//关闭
close(fd);
return 0;
}
再写个读的
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
if(argc!=2)
{
printf("%s filename.\n",argv[0]);
return -1;
}
int fd = open(argv[1],O_RDONLY);
//读,输出到屏幕
char buf[256];
int ret=0;
do
{
//从fd中读,读到buf中,最多读sizeof(buf)个字节。
//fd会递增的,最终指向文件末尾
ret=read(fd,buf,sizeof(buf));
//STDOUT_FILENO是标准输出的宏定义,值为1
write(STDOUT_FILENO,buf,ret);
}while(ret>0);//代表读到末尾了
close(fd);
return 0;
}
编译执行,那到底应该先执行哪一个呢?
通过man 2 open可以看到这么一段话。
补充:
使用man 7 fifo
可以查看FIFO的详细信息。
mmap共享映射区
mmap的说明
假设磁盘上有文件file.txt,mmap的作用是在内存中划分一块区域,然后将这个文件的某一段(部分)映射到内存。这样直接操作映射(内存)区域之后,就可以更改文件的内容。映射时,需要指定offset和length,因为使用内存,所以mmap这种进程间通信的方式的速度最快。
mmap函数
看看mmap函数的使用方法。
参数:
addr:传NULL。
length:映射区的长度。
prot:
一般使用PROT_READ(可读)和PROT_WRITE(可写)。
flags:
MAP_SHARED:映射区是共享的。对内存的修改会影响到源文件
MAP_PRIVATE:映射区是私有的。对内存的修改不会影响到源文件
fd:文件描述符,open打开一个文件。
offset:偏移量,从文件的哪部分开始映射。
返回值:
成功:返回可用的内存首地址。
失败:返回MAP_FAILED
munmap函数
释放映射区。
参数:
addr:传mmap的返回值。
length:mmap创建的映射区的长度。
返回值:
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
int main()
{
//在当前目录下新建一个mem.txt的文件,然后在里面写一些内容
int fd = open("mem.txt",O_RDWR);
char* mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(mem == MAP_FAILED)
{
perror("mmap error.");
return -1;
}
//修改内存区域相当于修改文件
strcpy(mem,"hello" );
//释放mmap
munmap(mem,8);
close(fd);
return 0;
}
mmap注意事项(一)
1、如果更改mem变量的地址,释放的时候munmap,传入mem还能成功吗?
答:不能
2、如果对mem越界操作会怎么样,比如length传8个字节,修改的却不止8个?
答:文件的大小对映射区操作有影响,应该避免。
比如文件大小只有8个字节,而你要往里写10个字节的数据,结果是只能写进8个字节。
3、如果文件偏移量随便填个数会怎么样?
现在,我们新建一个相对大一些的文件。
怎么做呢?
使用命令ps aux > mem.txt
这条命令的含义是将ps aux的结果输出到mem.txt这个文件。
再来看下这个文件的信息:
然后将程序中offset修改为1000试试:
char* mem = mmap(NULL,8,PROT_READ|PROT_WRITE,MAP_SHARED,fd,1000);
答:报错,原因是offset必须是 4K (4096) 的整数倍。
4、如果文件描述符先关闭,对mmap映射有没有影响?
答:没有影响。
5、open的时候,可以新创建一个文件来创建映射区吗?
将当前目录下的 “mem.txt” 给删除,然后在程序中写打开文件时创建。
结果运行时发生总线错误。
这说明,新建一个大小为0的文件是不可以的。
但是,如果在新建时给这个文件一定大小呢,再试试。
使用函数ftruncate()给这个文件一定的大小。
这里还需要注意的就是
1、mmap的length参数不能大于文件的大小,
2、mmap的offset参数不能大于文件的大小。
否则也会报错。
上面的mmap函数的offset参数改成0后,程序运行正常。
函数ftruncate()
函数功能:改变文件大小
函数原型:int ftruncate(int fd, off_t length)
函数说明:ftruncate()会将参数fd指定的文件大小改为参数length指定的大小。参数fd为已打开的文件描述词,而且必须是以写入模式打开的文件。如果原来的文件件大小比参数length大,则超过的部分会被删去。
返 回 值:0、-1
错误原因:
EBADF: 参数fd文件描述词为无效的或该文件已关闭。
EINVAL: 参数fd为一socket并非文件,或是该文件并非以写入模式打开。
mmap注意事项(二)
6、open的时候,只选择O_WRONLY可以吗?
运行程序:
所以,不可以。
7、open的时候,只选择O_RDONLY可以吗?
运行程序:
所以,也不可以。
8、当选择MAP_SHARED的时候,open文件选择O_RDONLY,prot选择PROT_READ|PROT_WRITE可以吗?
那怎样才能可以呢?
答:当选择 MAP_SHARED的时候,open文件时的权限必须大于或等于prot的权限。
注意:使用mmap这个函数时一定要判断返回值。
mmap实现父子进程通信
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
//先创建映射区
int fd =open("mem.txt",O_RDWR);
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(mem == MAP_FAILED)
{
perror("mmap error");
return -1;
}
//fork进程
pid_t pid = fork();
if(pid == 0)
{
//son
*mem=100;
printf("child,*mem = %d.\n",*mem);
sleep(3);
printf("child,*mem = %d.\n",*mem);
}
else if(pid>0)
{
//parent
sleep(1);
printf("parent,*mem = %d.\n",*mem);
*mem = 1001;
printf("parent,*mem = %d.\n",*mem);
wait(NULL);
}
munmap(mem,4);
close(fd);
return 0;
}
匿名映射
通过使用我们发现,使用映射区来完成文件读写操作十分方便,父子进程间通信也较容易。但缺陷是,每次创建映射区一定要依赖一个文件才能实现。通常为了建立映射区要open一个temp文件,创建好了再unlink、close掉,比较麻烦。 可以直接使用匿名映射来代替。
Linux系统给我们提供了创建匿名映射区的方法,无需依赖一个文件即可创建映射区。但需要借助标志位参数flags来指定。
这个标志位参数为:MAP_ANONYMOUS (或它的简写MAP_ANON)。并且此时fd传参-1。
用法举例如下:
int *p = mmap(NULL, 4, PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);
使用匿名映射将上个代码进行改写。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
int main()
{
int *mem = mmap(NULL,4,PROT_READ|PROT_WRITE,MAP_SHARED|MAP_ANON,-1,0);
if(mem == MAP_FAILED)
{
perror("mmap error");
return -1;
}
//fork进程
pid_t pid = fork();
if(pid == 0)
{
//son
*mem=100;
printf("child,*mem = %d.\n",*mem);
sleep(3);
printf("child,*mem = %d.\n",*mem);
}
else if(pid>0)
{
//parent
sleep(1);
printf("parent,*mem = %d.\n",*mem);
*mem = 1001;
printf("parent,*mem = %d.\n",*mem);
wait(NULL);
}
munmap(mem,4);
return 0;
}
需注意的是,MAP_ANONYMOUS和MAP_ANON这两个宏是Linux操作系统特有的宏。在unix和类Unix操作系统中如无该宏定义。
其实可以在unix和类Unix操作系统中使用如下两步来完成匿名映射区的建立:
fd = open("/dev/zero", O_RDWR);
p = mmap(NULL, size, PROT_READ|PROT_WRITE, MMAP_SHARED, fd, 0);
Linux系统中的两个设备文件:
/dev/zero,聚宝盆,可以随意映射。
/dev/null,无底洞,一般错误信息重定向到该文件中。
mmap实现无血缘关系的进程间通信
进程之间要想通信的话,mmap函数中的flags参数必须设置为MMAP_SHARED。
写(文件)进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}STU;
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("%s\n",argv[0]);
return -1;
}
//1.open file
int fd =open(argv[1],O_RDWR|O_CREAT|O_TRUNC,0666);
int length = sizeof(STU);
ftruncate(fd,length);
//2.mmap
STU *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED)
{
perror("mmap error");
return -1;
}
int num = 1;
//3.修改内存数据
while(1)
{
stu->sid =num;
sprintf(stu->sname,"child-%03d",num++);
sleep(1);
}
//4.释放映射区和关闭文件描述符
munmap(stu,length);
close(fd);
return 0;
}
读(文件)进程
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
typedef struct _Student{
int sid;
char sname[20];
}STU;
int main(int argc,char* argv[])
{
if(argc!=2)
{
printf("%s\n",argv[0]);
return -1;
}
//1.open file
int fd =open(argv[1],O_RDWR);
int length = sizeof(STU);
//2.mmap
STU *stu = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
if(stu == MAP_FAILED)
{
perror("mmap error");
return -1;
}
//3.读取内存数据
while(1)
{
printf("sid = %d,sname = %s.\n",stu->sid,stu->sname);
sleep(1);
}
//4.释放映射区和关闭文件描述符
munmap(stu,length);
close(fd);
return 0;
}
作业
1、通过命名管道传输数据,进程A和进程B,进程A将一个文件(MP3)发送给进程B。
程序设计思路大概是这样的.
1、先在当前目录下创建一个FIFO文件;
2、复制一个MP3文件到当前路径下。
3、write.c中读取该MP3文件并写入到FIFO中。与此同时,read.c中读取FIFO并写到新建的mp3文件中(用open函数打开或创建的方式)
读取mp3文件并写入到FIFO文件中
write.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
//通过命名管道传输数据,进程A和进程B,进程A将一个文件(MP3)发送给进程B。
int main(int argc,char *argv[])
{
if(argc!=3)
{
printf("%s mp3_filepath fifo_filepath",argv[0]);
return -1;
}
//读取mp3文件内容然后写入FIFO中,边读边写。
//当前目录有一个fifo文件,否则还要创建
//以只读方式打开mp3文件
int fd_mp3 = open(argv[1],O_RDONLY);
//以只写方式打开fifo文件
int fd_fifo = open(argv[2],O_WRONLY);
//写到FIFO中
char buf[4096];
int ret=0;
do
{
//从fd_mp3中读,读到buf中,最多读sizeof(buf)个字节。
//fd_mp3会递增的,最终指向文件末尾
ret=read(fd_mp3,buf,sizeof(buf));
//测试是否不管文件中还剩多少字节,每次都读count
write(fd_fifo,buf,ret);
}while(ret>0&&ret==sizeof(buf));//代表读到末尾了
//关闭fifo
close(fd_fifo);
//关闭mp3
close(fd_mp3);
return 0;
}
读取FIFO文件并写入到新建的mp3文件中
read.c
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
if(argc!=3)
{
printf("%s mp3_filename fifo_filename.\n",argv[0]);
return -1;
}
int fd_mp3 = open(argv[1],O_RDWR|O_CREAT,0666);
int fd_fifo = open(argv[2],O_RDONLY);
char buf[4096];
int ret=0;
do
{
ret=read(fd_fifo,buf,sizeof(buf));
write(fd_mp3,buf,ret);
}while(ret>0);//代表读到末尾了
close(fd_fifo);
close(fd_mp3);
return 0;
}
2、实现多进程拷贝文件。
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/mman.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <string.h>
#include <stdlib.h>
int main(int argc,char* argv[])
{
//默认5个进程
int n=5;
//输入参数至少是3,第四个参数可以是进程数
if(argc<3)
{
printf("%s sourcefilepath destinationfilepath pidnum.\n",argv[0]);
return -1;
}
if(argc == 4)
{
n = atoi(argv[3]);
}
//open source file
int srcfd =open(argv[1],O_RDONLY);
if(srcfd < 0)
{
perror("open source error.\n");
exit(1);
}
//open destination file
int dstfd = open(argv[2],O_RDWR|O_CREAT|O_TRUNC,0666);
if(dstfd<0)
{
perror("open destination error.\n");
exit(1);
}
//使用stat函数从源文件获得文件大小
struct stat sb;
stat(argv[1],&sb);
int length = sb.st_size;
truncate(argv[2],length);
//将源文件映射到缓冲区
//mmap
char* psrc = mmap(NULL,length,PROT_READ,MAP_SHARED,srcfd,0);
if(psrc == MAP_FAILED)
{
perror("mmap src error.\n");
exit(1);
}
//将目标文件映射到缓冲区
//mmap
char* pdst = mmap(NULL,length,PROT_READ|PROT_WRITE,MAP_SHARED,dstfd,0);
if(pdst == MAP_FAILED)
{
perror("mmap src error.\n");
exit(1);
}
//创建多个进程
int i;
for(i=0;i<n;i++)
{
if(fork()==0)
break;
}
//计算子进程需要拷贝的起点和大小
int cpsize = length / n;
int mod = length % n;
//数据拷贝,memcpy
if(i<n)//子进程
{
if(i == n-1)//最后一个子进程
{
//void *memcpy(void *dest, const void *src, size_t n);
//从存储区str2复制n个字节到存储区str1
memcpy(pdst+i*cpsize,psrc+i*cpsize,cpsize+mod);
}
else
{
memcpy(pdst+i*cpsize,psrc+i*cpsize,cpsize);
}
}
else//父进程回收子进程
{
for(i=0;i<n;i++)
{
wait(NULL);
}
}
//释放映射区和关闭文件描述符
if(munmap(psrc,length)<0)
{
perror("munmap source error.\n");
exit(1);
}
if(munmap(pdst,length)<0)
{
perror("munmap destination error.\n");
exit(1);
}
close(srcfd);
close(dstfd);
return 0;
}