进程之间的通信主要是为了:
- 数据传输
- 资源共享
- 事件通知
- 进程控制
Linux系统下的ipc
(1)早期unix系统ipc
- 管道
- 信号
- fifo
(2)system-v ipc(贝尔实验室)
- system-v 消息队列
- system-v 信号量
- system-v 共享内存
(3)socket ipc(BSD)
(4)posix ipc(IEEE)
- posix 消息队列
- posix 信号量
- posix 共享内存
当数据从一个进程连接流到另一个进程, 这之间的连接就是一个管道(pipe),我们通常是把一个进程的输出通过管道连接到另一个进程的输入 管道本质上也是一个文件
shell 命令来说,命令的连接是通过管道字符来完成的 如 ps -aux | grep root 只需要使用“|”字符进行连接即可
执行以下过程: 1)shell 负责安排两个命令的标准输入和标准输出。 2)ps 的标准输入来自终端鼠标、键盘等。 3)ps 的标准输出传递给 grep,作为 grep 的标准输入。 4) grep 的标准输出连接到终端,即输出到显示器屏幕,最终我们看到 grep 的输出结果。 管道本质上也是一个文件,上图的过程可以看作是 ps 进程将输出的内容写入管道中, grep进程从管道中读取数据,可以把它抽象成一个可读写的文件。遵循了 Linux 中“一切皆文件”的设计思想,它借助 VFS(虚拟文件系统)给应用程序提供操作接口,实现了管道的功能
注: 虽然管道的实现形态上是文件,但是管道本身并不占用磁盘或者其他外部 存储的空间,它占用的是内存空间,因此 Linux 上的管道就是一个操作方式为文件的内存缓冲区
Linux 系统上的管道分两种类型: 1.匿名管道 (无名管道) 2.命名管道 (有名管道)
无名管道: 匿名管道最常见的形态就是我们在 shell 操作中最常用的“|”。它的特点是只能在父子进程中使用,父进程在产生子进程前必须打开一个管道文件,然后 fork 产生子进程,这样子进程通过拷贝父进程的进程地址空间获得同一个管道文件的描述符,以达到使用同一个管道通信的目的
匿名管道(PIPE)是一种特殊的文件,但虽然它是一种文件,却没有名字,因此一般进程无法使用 open() 来获取他的描述符,它只能在一个进程中被创建出来,然后通过继承的方式将他的文件描述符传递给子进程,这就是为什么匿名管道只能用于亲缘关系进程间通信的原因
匿名管道不同于一般文件的显著之处是:它有两个文件描述符,而不是一个,一个只能用来读,另一个只能用来写,这就是所谓的“半双工”通信方式。而且它对写操作不做任何保护
匿名管道只能用于一对一的亲缘进程通信
匿名管道不能使用 lseek() 来进行所谓的定位,因为他们的数据不像普通文件那样按块的方式存放在诸如硬盘、 flash 等块设备
特点- 特殊文件(没有名字),无法使用open,但是可以使用close。
- 只能通过子进程继承文件描述符的形式来使用
- write和read操作可能会阻塞进程
- 所有文件描述符被关闭之后,无名管道被销毁
pipe() 函数 创建一个匿名管道,一个可用于进程间通信的单向数据通道
#include
int pipe(int pipefd[2]);
注:数组 pipefd 是用于返回两个引用管道末端的文件描述符,它是一个由两个文件描述符组成的数组的指针。 pipefd[0] 指管道的读取端, pipefd[1]指向管道的写端
向管道的写入端写入数据将会由内核缓冲,即写入内存中,直到从管道的读取端读取数据为止,而且数据遵循先进先出原则。 pipe() 函数还会返回一个 int 类型的变量,如果为0 则表示创建匿名管道成功,如果为-1 则表示创建失败,并且设置 errno
使用步骤- 父进程pipe无名管道
- fork子进程
- close无用端口
- write/read读写端口
- close读写端口
(1) :定义一个数组 pipe_fd,在创建匿名管道后通过数组返回管道的文件描述符。 • (2) :调用 pipe() 创建一个匿名管道,创建成功则得到两个文件描述符 pipe_fd[0]、 pipe_fd[1], 否则返回-1。 • (3) :调用 fork() 创建一个子进程,如果返回值是 0 则表示此时运行的是子进程,那么在子 进程中调用 close() 函数关闭写描述符,并使子进程睡眠 3s 等待父进程已关闭相应的读描述 符。 • (4) :子进程调用 read() 函数读取管道内容,如果管道没有数据则子进程将被阻塞,读取到 数据就将数据打印出来。特别地如果调用 read() 函数读取一个关闭了写描述符的管道,那 么 read() 会返回 0,(本例子中父进程的写描述符没有关闭)。 • (5) :调用 close() 函数关闭子进程读描述符。 • (6) :如果 fork() 函数的返回值大于 0,则表示此时运行的是父进程,那么在父进程中先调 用 close() 关闭管道的读描述符,并且等待 1s,因为此时可能子进程先于父进程运行,暂且 等待一会。 • (7) :父进程调用 write() 函数将数据写入管道。 • (8) :关闭父进程写描述符。 • (9) :调用 waitpid() 函数收集子进程退出信息并退出进程。
有名管道 命名管道(FIFO)与匿名管道(PIPE)是不同的,命名管道可以在多个无关的进程中交换数据(通信)。
命名管道不同于无名管道之处在于它提供了一个路径名与之关联,以一个文件形式存在于文件系统中,这样,即使与命名管道的创建进程不存在“血缘关系”的进程,只要可以访问该命名管道文件的路径,就能够彼此通过命名管道相互通信,因为可以通过文件的形式,那么就可以调用系统中对文件的操作,如打开(open)、读(read)、写(write)、关闭(close)等函数,虽然命名管道文件存储在文件系统中,但数据却是存在于内存中的
特点- 有文件名,可以使用open函数打开
- 任意进程间数据传输
- write和read操作可能会阻塞进程
- write具有"原子性"(会检验数据是否写得下,若写的下才写,否则不写)
在shell 中创建管道可以使用mkfifo命令
mkfifo test //创建一个test 管道
file test //查看文件类型
test: fifo (named pipe)
mkfifo函数 头文件:
#include
#include
函数原型:
int mkfifo(const char *filename,mode_t mode)
mkfifo() 会根据参数 pathname 建立特殊的 FIFO 文件,而参数 mode 为该文件的模式与权限
mkfifo() 创建的 FIFO 文件其他进程都可以进行读写操作,可以使用读写一般文件的方式操作它,如 open, read, write, close 等
mode 模式及权限参数说明:
• O_RDONLY:读管道。
• O_WRONLY:写管道。
• O_RDWR:读写管道。
• O_NONBLOCK:非阻塞。
• O_CREAT:如果该文件不存在,那么就创建一个新的文件,并用第三个参数为其设置权限。
• O_EXCL:如果使用 O_CREAT 时文件存在,那么可返回错误消息。这一参数可测试文件是
否存在。
函数返回值说明如下:
• 0:成功
• EACCESS:参数 filename 所指定的目录路径无可执行的权限。
• EEXIST:参数 filename 所指定的文件已存在。
• ENAMETOOLONG:参数 filename 的路径名称太长。
• ENOENT:参数 filename 包含的目录不存在。
• ENOSPC:文件系统的剩余空间不足。
• ENOTDIR:参数 filename 路径中的目录存在但却非真正的目录。
• EROFS:参数 filename 指定的文件存在于只读文件系统内。
使用 FIFO 的过程中,当一个进程对管道进行读操作时: 1)若该管道是阻塞类型,且当前 FIFO 内没有数据,则对读进程而言将一直阻塞到有数据写入 2)若该管道是非阻塞类型,则不论 FIFO 内是否有数据,读进程都会立即执行读操作。即如果FIFO 内没有数据,读函数将立刻返回 0
使用 FIFO 的过程中,当一个进程对管道进行写操作时: 1)若该管道是阻塞类型,则写操作将一直阻塞到数据可以被写入。 2)若该管道是非阻塞类型而不能写入全部数据,则写操作进行部分写入或者调用失败
使用步骤- 第一个进程mkfifo有名管道
- open有名管道,write/read数据
- close有名管道
- 第二个进程open有名管道,read/write数据
- close有名管道
#include
#include
#include
#include
#include
#include
#include
#include
#include
#include
#defineMYFIFO"myfifo"/*命名管道文件名*/
#defineMAX_BUFFER_SIZEPIPE_BUF/*4096定义在于limits.h中*/
voidfifo_read(void)
{
char buff[MAX_BUFFER_SIZE];
int fd;
int nread;
printf("*****************readfifo************************\n");
/*判断命名管道是否已存在,若尚未创建,则以相应的权限创建*/
if(access(MYFIFO,F_OK)==-1)
{
if((mkfifo(MYFIFO,0666)0)
{
printf("Read'%s'fromFIFO\n",buff);
}
printf("*****************closefifo************************\n");
close(fd);//(8)
exit(0);
}
voidfifo_write(void)
{
int fd;
char buff[]="thisisafifotestdemo";
int nwrite;
sleep(2);//等待子进程先运行//(9)
/*以只写阻塞方式打开FIFO管道*/
fd=open(MYFIFO,O_WRONLY|O_CREAT,0644);
if(fd==-1)
{
printf("Openfifofileerror\n");
exit(1);}
printf("Write'%s'toFIFO\n",buff);
/*向管道中写入字符串*/
nwrite=write(fd,buff,MAX_BUFFER_SIZE);
if(wait(NULL))
{
close(fd);
exit(0);
}
}
int main()
{
pid_tresult;
/*调用fork()函数*/
result=fork();//(1)
/*通过result的值来判断fork()函数的返回情况,首先进行出错处理*/
if(result==-1)
{
printf("Forkerror\n");
}
elseif(result==0)/*返回值为0代表子进程*/
{
fifo_read();//(2)
}
else/*返回值大于0代表父进程*/
{
fifo_write();//(3)
}
return result;
}
(1):首先使用fork函数创建一个子进程。
•(2):返回值为0代表子进程,就运行fifo_read()函数。
•(3):返回值大于0代表父进程,就运行fifo_write()函数。
•(4):在子进程中先通过access()函数判断命名管道是否已存在,若尚未创建,则以相应的权限创建
(5):调用mkfifo()函数创建一个命名管道。
•(6):使用open()函数以只读阻塞方式打开命名管道 。 •(7):使用read()函数读取管道的内容,由于打开的管道是阻塞的,而此时管道中没有存在任何数据,因此子进程会阻塞在这里,等待到管道中有数据时才恢复运行,并打印从管道中读取到的数据。
•(8):读取完毕,使用close()函数关闭管道。
•(9):父进程休眠2秒,等待子进程先运行,因为本例子是在子进程中创建管道的。
•(10):以只写阻塞方式打开FIFO管道。
•(11):向管道中写入字符串数据,当写入后管道中就存在数据了,此时处于阻塞的子进程将恢复运行,并将字符串数据打印出来。
•(12):等待子进程退出,并且关闭管道。
其他函数:(1)memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。
将s中当前位置后面的n个字节用ch替换并返回s
#include
void *memset(void *s, int ch, size_t n);
(2)read()函数 函数原型
ssize_t read(int fd,void*buf,size_t count)
fd: 是文件描述符,对应0
buf: 为读出数据的缓冲区;
count: 为每次读取的字节数(是请求读取的字节数,读上来的数据保
存在缓冲区buf中,同时文件的当前读写位置向后移)
(3)write()函数
ssize_t write(int fd,void*buf,size_t count)
fd: 是文件描述符,对应1
buf: 需要写入的数据,通常为字符串;
count: 每次写入的字节数