linux - 进程间通信教程
进程间通信
概念
linux环境下,进程地址空间相互独立,每个进程都有各自不同的用户地址空间。任何一个进程的全局变量在另一个进程中看不到,所以进程和进程之间不能相互访问,要交换数据必须通过内核;在内核中开辟一块缓冲区,进程A把数据从用户空间拷贝到内核缓冲区,进程B再从内核缓冲区把数据读取,内核提供这种机制成为进程间通信,简称IPC;
通信方式
如:文件、管道、信号、共享内存、消息队列、套接字、命名套接字;
常用的包括不限于:
- 管道
- 信号
- 共享映射区
- 本地套接字
管道
概念
管道是一种最基本的ipc机制,也称匿名管道,应用于有血缘关系的进程之间,完成数据传递。调用pipe函数即可创建一个管道;
管道的特性:
- 管道的本质是一块内核缓冲区
- 由两个文件描述符引用,一个表示写,一个表示读
- 规定数据从管道的写端流入管道,从读端流出。
- 两个进程都终结的时候,管道也自动消失。
- 管道的读端和写端默认都是阻塞的。
- 默认缓冲区大小为4K,可以使用ulimit -a命令获取大小。
局限性
- 数据一旦被读走,便不在管道中存在,不可反复读取。
- 数据只能在一个方向上流动,若要实现双向流动,必须使用两个管道
- 只能在有血缘关系的进程间使用管道。
使用
pipe
创建一个管道,若函数调用成功,fd[0]存放管道的读端,fd[1]存放管道的写端;
#include <unistd.h>
int pipe(int pipefd[2]);
#define _GNU_SOURCE /* See feature_test_macros(7) */
#include <fcntl.h> /* Obtain O_* constant definitions */
#include <unistd.h>
int pipe2(int pipefd[2], int flags);
函数调用成功返回读端和写端的文件描述符,其中fd[0]是读端, fd[1]是写端,向管道读写数据是通过使用这两个文件描述符进行的,读写管道的实质是操作内核缓冲区。
父子进程管道通信
个进程在由pipe()创建管道后,一般再fork一个子进程,然后通过管道实现父子进程间的通信(因此也不难推出,只要两个进程中存在血缘关系,这里的血缘关系指的是具有共同的祖先,都可以采用管道方式来进行通信)。父子进程间具有相同的文件描述符,且指向同一个管道pipe,其他没有关系的进程不能获得pipe()产生的两个文件描述符,也就不能利用同一个管道进行通信。
第一步:父进程创建管道
第二步:父进程fork出子进程
第三步:父进程关闭fd[0],子进程关闭fd[1]
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
int main(int argc,char* args[])
{
int status;
//创建管道
int fd[2];
int ret = pipe(fd);
if(ret<0)
{
perror("pipe error");
return -1;
}
//创建子进程
pid_t pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)
{
//关闭读端
close(fd[0]);
sleep(5);
write(fd[1],"hello,world",strlen("hello,world"));
//wait(NULL);
waitpid(-1, &status, WNOHANG);
}
else
{
//关闭写端
close(fd[1]);
char buf[1024];
memset(buf,0,sizeof(buf));
int n = read(fd[0],buf,sizeof(buf));
printf("read over:%s\n",buf);
}
return 0;
}
ps -aux
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
int main(int argc,char* args[])
{
int status;
//创建管道
int fd[2];
int ret = pipe(fd);
if(ret<0)
{
perror("pipe error");
return -1;
}
//创建子进程
pid_t pid = fork();
if(pid<0)
{
perror("fork error");
return -1;
}
else if(pid>0)
{
//关闭读端
close(fd[0]);
//将标准输出重定向到管道的写端
dup2(fd[1],STDOUT_FILENO);
execlp("ps","ps","aux",NULL);
perror("execlp error");
wait(NULL);
}
else
{
//关闭写端
close(fd[1]);
//将标准输入重定向到管道的读端
dup2(fd[0],STDIN_FILENO);
execlp("grep","grep","bash",NULL);
perror("execlp error");
}
return 0;
}
管道读写
读操作
有数据
- read正常都,返回读出的字节大小
无数据
- 写端全部关闭,read解除阻塞,返回0,相当于读到了文件尾部;
- 写端没有全部关闭,read阻塞
写操作
读端全部关闭
- 管道破裂,进程终止,内核给当前线程发送SIGSTOP信号
读端没有全部关闭
缓冲区写满
- write阻塞
缓冲区没有满
- 继续write
兄弟进程通信
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
int fd[2]; //用来标记管道的两端的文件描述符
int ret = pipe(fd); //创建管道 pipe. fd[2] 是固定的输出参数
if (ret == -1) {
perror("[pipe create file] ");
return 0;
}
int pipeRead = fd[0];
int pipeWrite = fd[1];
int i = 0;
for ( ; i < 2; i++) {
pid_t pid = fork();
if (pid == 0)
break;
if (pid == -1)
perror("[creator process file:]");
}
if (i == 0) { //child process 1
dup2(pipeWrite, STDOUT_FILENO);
close(pipeRead);
execlp("ps", "ps", "aux", NULL);
} else if (i == 1) { //child process 2
dup2(pipeRead, STDIN_FILENO);
close(pipeWrite);
execlp("grep", "grep", "bash", "--color=auto", NULL);
} else if (i == 2) { //parent process
close(pipeWrite);
close(pipeRead);
// sleep(2);
int wpid;
while ( wpid = waitpid(-1, NULL, WNOHANG) != -1) { //回收子进程
if (wpid == 1) ///sbin/init splash 进程 /sbin/launchd
continue;
if (wpid == 0)
continue;
printf("child dide pid = %d\n", wpid);
}
}
printf("pipeWrite = %d, pipeRead = %d\n", pipeWrite, pipeRead);
return 0;
}
管道非阻塞
默认情况下,管道的读写两端都是阻塞的,若要设置读或者写端为非阻塞,则可参考下列三个步骤进行:
第1步: int flags = fcntl(fd[0], F\_GETFL, 0);
第2步: flag |= O\_NONBLOCK;
第3步: fcntl(fd[0], F\_SETFL, flags);
若是读端设置为非阻塞:
- 写端没有关闭,管道中没有数据可读,则read返回-1;
- 写端没有关闭,管道中有数据可读,则read返回实际读到的字节数
- 写端已经关闭,管道中有数据可读,则read返回实际读到的字节数
- 写端已经关闭,管道中没有数据可读,则read返回0
查看管道缓冲区大小
long fpathconf(int fd, int name);
long pathconf(const char *path, int name);
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
int main(void)
{
int fd[2];
int ret = pipe(fd);
int pipeSize = fpathconf(fd[0],_PC_PIPE_BUF);
printf("pipesize = %d\n",pipeSize);
return 0;
}
命名管道
概念
FIFO常被称为命名管道,以区分管道(pipe)。管道(pipe)只能用于“有血缘关系”的进程间通信。但通过FIFO,不相关的进程也能交换数据。
FIFO是Linux基础文件类型中的一种(文件类型为p,可通过ls -l查看文件类型)。但FIFO文件在磁盘上没有数据块,文件大小为0,仅仅用来标识内核中一条通道。进程可以打开这个文件进行read/write,实际上是在读写内核缓冲区,这样就实现了进程间通信。
创建管道
适用于无血缘关系的进程之间的通信。进程之间是不要使用 sleep() 函数的,因为管道默认就是堵塞的。虽然实现形态上是文件,但是管道本身并不占用磁盘或者其他外部存储的空间。在Linux的实现上,它占用的是内核的一段缓冲区空间。
#include <sys/types.h>
#include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
#include <fcntl.h> /* Definition of AT_* constants */
#include <sys/stat.h>
int mkfifoat(int dirfd, const char *pathname, mode_t mode);
fifo进程通信
//fifo_read.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main()
{
//int mkfifo(const char *pathname, mode_t mode);
//创建fifo文件
/*int ret = mkfifo("./myfifo",0777);
if(ret<0)
{
perror("fifo error");
return -1;
}*/
//打开文件
int fd = open("./myfifo",O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
char buf[64];
memset(buf,0,sizeof(buf));
int n = read(fd,buf,sizeof(buf));
printf("%s\n",buf);
//关闭文件
close(fd);
getchar();
return 0;
}
//fifo_write.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <unistd.h>
#include <errno.h>
#include <fcntl.h>
#include <ctype.h>
#include <sys/wait.h>
#include <sys/stat.h>
int main()
{
//int mkfifo(const char *pathname, mode_t mode);
//创建fifo文件
int ret = mkfifo("./myfifo",0777);
if(ret<0)
{
perror("fifo error");
return -1;
}
//打开文件
int fd = open("./myfifo",O_RDWR);
if(fd<0)
{
perror("open error");
return -1;
}
//写文件
write(fd,"hello,world",strlen("hello,world"));
//关闭文件
//close(fd);
getchar();
return 0;
}