Linux环境下,各进程地址空间相互独立,任何一个进程内的变量在另一个进程中都是不可见的,所以进程之间是不能访问的,要交换数据必须通过内核,在内核中开辟一块缓冲区,进程1把数据从用户空间拷到内核缓冲区,进程2在从中读走,这个就叫做进程通信,简称IPC。
管道pipe:最基本的IPC机制,也称为匿名管道,适用于有血缘关系的进程之间的通信,就是父子进程之间的通信。
命令管道fifo:既可用于有血缘关系的进程之间的通信,又可以用于没有血缘关系的进程之间的通信。
内存映射区mmap:既可用于有血缘关系进程之间的通信也可用于没有血缘关系进程之间的通信。
管道的本质是一块内核缓冲区,由两个文件描述符组成,一个表示写端,一个表示读端,规定数据从管道的写端流入管道,读端流出管道,当两个进程都退出了的时候,这个时候管道也会消失,注意管道的读端和写端默认都是阻塞的。管道是半双工的,就是数据只能在一个方向上流动。
函数原型如上,使用的时候传入一个大小为2的数组即可,fd[0]表示读端,fd[1]表示写端;如果创建成功则返回0,创建失败则返回-1,下面就来看看具体的使用,如何用pipe实现进程父子进程之间的通信:
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd[2];
int pip=pipe(fd);
if(pip<0)
{
perror("pipe error\n");
}
pid_t pid = fork();
if(pid<0)
{
perror("fork error\n");
return -1;
}
else if(pid>0)//在父进程中往管道写数据
{
close(fd[0]);
write(fd[1],"HELLO WORLD",sizeof("HELLO WORLD"));
close(fd[1]);
}
else if(pid==0)//子进程在管道中读数据
{
close(fd[1]);
char buf[BUFSIZ];
memset(buf,0,sizeof(buf));
int size=read(fd[0],buf,sizeof(buf));
printf("读到的数据:size=%d,contents=%s\n",size,buf);
}
return 0;
}
在父进程中对管道进行写操作,在子进程中对管道进行读操作,注意:读的时候需要把写端关闭,写的时候需要把读端关闭,只允许开一端。结合前面的fork函数的使用,还有要注意头文件的使用。上面的过程可以总结为:父进程创建pipe,父进程创建子进程,父进程关闭一端,子进程关闭一端,父子进程调用read,write函数实现通信。
管道通信是阻塞的,如果我们只对其一端进行操作,会出现那些情况呢?首先是关闭读端,然后一直写入,这个时候会把管道写满,最后返回一个SIGPIPE信号:
#include
#include
#include
#include
#include
#include
#include
void handler(int signo)
{
printf("signo=[%d]\n",signo);
}
int main()
{
//signal(SIGPIPE,handler);
struct sigaction act;
act.sa_handler=handler;
sigemptyset(&act.sa_mask);
act.sa_flags=0;
sigaction(SIGPIPE,&act,NULL);
int fd[2];
int pip=pipe(fd);
if(pip<0)
{
perror("fork error\n");
return -1;
}
close(fd[0]);
while(1)
{
write(fd[1],"HELLO WORLD",sizeof("HELLO WORLD"));
}
return 0;
}
这里我们用到了信号注册函数sigaction,当然使用signal也是可以的,反正这里只是简单的捕捉一下返回的信号,在这里我们先创建一个管道,关闭读端,对其循环写,观察某个时刻当其写满了是否会返会SIGPIPE信号,这个信号就表示管道已经破裂。
运行结果中返回的全部都是signo=13,这个13就是SIGPIPE ,我们可以通过kill -l查看所有的信号的值:
对于读端没有关闭,而没有进行读取时,管道会被写满,此时会被阻塞。当管道阻塞以后,其实也可以手动把他设置为非阻塞,前面已经了解过了fcntl函数,这个函数可以获得文件的flag属性并能修改其flag属性:
//读端设置为非阻塞
int flag=fcntl(fd[0],F_GETFL,0);
flag = flag | O_NOBLOCK;
fcntl(fd[0],F_SETFL,flag);
//写端设置为非阻塞
int flag=fcntl(fd[1],F_GET1FL,0);
flag = flag | O_NOBLOCK;
fcntl(fd[1],F_SETFL,flag);
在默认的情况下,管道的两端都是阻塞的。
FIFO用于没有没有血缘关系的两个进程之间的通信可以参考以下步骤,一个进程创建一个FIFO文件并对其进行写操作,另一个进程打开FIFO文件,进行读取;创建FIFO文件调用mkfio函数或者是命令都行,在这里我们就使用mkfifo函数,函数原型如下,第一个是文件名称,第二个是创建的FIFO文件的权限,一般可以将其设置为0777.成功创建则返回0,创建失败就会返回-1.
下面来看一下写端进程的函数如何去写:按照上面的模板,先创建FIFO文件,然后再进行读写操作,最后记得需要保持窗口不能让程序一执行就直接退出了,因此这里在写进程中用到了getchar(),还有就是在写进程中不要关闭文件,否则就读不到数据了。
#include
#include
#include
#include
#include
#include
#include
int main()
{
int result = mkfifo("./myfifo",0777);
if(result<0)
{
perror("mkfifo error\n");
return -1;
}
int fd=open("./myfifo",O_RDWR);
if(fd<0)
{
perror("open error\n");
return -1;
}
write(fd,"HELLO WORLD",sizeof("HELLO WORLD"));
getchar();
return 0;
}
读端相较来说比较简单,只要读取文件的那种操作就行:
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("./myfifo",O_RDWR);
if(fd<0)
{
perror("open error\n");
return -1;
i}
char buf[BUFSIZ];
memset(buf,0,sizeof(buf));
int ret=read(fd,buf,sizeof(buf));
printf("收到消息:%s,size=%d\n",buf,ret);
close(fd);
return 0;
}
注意在读进程的最后需要把文件关闭,还有就是这里头文件的引用有很多,容易漏掉。
运行的时候我们需要打开两个窗口,一个打开读端,一个打开写端,但是运行一次当前目录就会多一个myfifo文件,下次执行的时候必须要把这个文件给删了才能继续运行,这样就显得很复杂了,因此我们需要在写进程中添加判断,在这个文件不存在的时候才进行创建,这里就需要用到access函数来判断文件是否存在,如果文件存在函数就会返回0,不存在就会返回-1。
对写进程作如下修改,加上一个判断条件,access函数的第二个参数F_OK 就是专门用来判断文件是否存在的。当然,access函数也可以用来判断文件具有那些权限,具体读者可以通过查询手册了解。
if(access("./myfifo",F_OK)!=0)
{
int result = mkfifo("./myfifo",0777);
if(result<0)
{
perror("mkfifo error\n");
return -1;
}
}
修改之后就不用慢慢删除了,就可以正常的运行来了。
共享映射区就是将一个磁盘文件与存储空间中的一个缓冲区相映射。这样从缓冲区中取数据就相当于从文件中读取相应字节数;将数据写入缓冲区会根据传入的参数判定是否将写的内容写入到文件中。
上面是mmap的函数原型,注意使用的时候头文件要加上
写进程:
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("./txt.log",O_RDWR);
if(fd<0)
{
perror("open error\n");
return -1;
}
int len=lseek(fd,0,SEEK_END);
void *addr=mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED)
{
perror("mmap errro\n");
return -1;
}
memcpy(addr,"HELLO WORLD",sizeof("HELLO WORLD"));
getchar();
close(fd);
return 0;
}
读进程:
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd=open("./txt.log",O_RDWR);
if(fd<0)
{
perror("open error\n");
return -1;
}
int len=lseek(fd,0,SEEK_END);
void *addr=mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED)
{
perror("mmap errro\n");
return -1;
}
char *str=(char*)addr;
printf("读到内容:%s\n",str);
close(fd);
return 0;
}
一看发现这两个进程的代码基本上都是一样的,只有后面的读写操作不同,注意这个打开的文件一定是已经存在的,并且里面是有内容的,就是文件的大小不能为0。
这里我们使用的是MAP_SHARED,我们会发现文件已经被修改了(刚开始里面是随机写入的内容)。
下面看mmap实现父子进程之间的通信:
#include
#include
#include
#include
#include
#include
#include
#include
int main()
{
int fd = open("./txt.log",O_RDWR);
if(fd<0)
{
perror("open error\n");
return -1;
}
int len=lseek(fd,0,SEEK_END);
void *addr = mmap(NULL,len,PROT_READ | PROT_WRITE,MAP_SHARED,fd,0);
if(addr==MAP_FAILED)
{
perror("mmap error\n");
return -1;
}
pid_t pid=fork();
if(pid<0)
{
perror("fork error\n");
return -1;
}
else if(pid>0)
{
memcpy(addr,"haha",sizeof("haha"));
wait(NULL);
}
else if(pid==0)
{
sleep(1);
char *str = (char*)addr;
printf("读到的数据:%s\n",str);
}
return 0;
}
要先建立映射区,然后才能进行通信,注意,在这里使用MAP_PRIVATE是无法完成通信的,必须使用MAP_SHARED.子进程处的sleep(1)保证父进程先执行,父进程wait函数保证不会产生僵尸进程,这都是一些需要注意的小点。
mmap还可以用作匿名映射区:MAP_ANONYMOUS需要和MAP_SHARED一起使用。fd要使用-1。
void *addr=mmap(NULL,4096,PROT_READ | PROT_WRITE,MAP_SHARED | MAP_ANONYMOUS,-1,0);
只要将创建映射区的那行代码修改就可,这种匿名映射的方式还是很常用的,毕竟不用创建文件,操作相对简单。注意这个只能用于有血缘关系的进程之间的通信。