前言
进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?首先,进程间通信至少可以通过传送、打开文件来实现,不同的进程通过一个或者多个文件来传递信息
,事实上,在很多应用系统都是用了这种方法。UNIX系统中实现进程间通信的方法有很多,而且不幸的是,极少的方法能在所有的UNIX系统中进行移植(唯一一种是半双工的管道
,这也是最原始的一种通信方式)。而Linux作为一种新兴的操作系统,几乎支持所有的UNIX下常用的进程间通信方法:管道、消息队列、共享内存、信号量、套接字等。其实前面四种主要用于同一台机器上
的进程间通信,而套接字则主要用于不同机器之间
的网络通信。本文主要讲的是同一台机器上的进程间通信。
管道
前面也说到父子进程之间并不共享数据段和堆栈段,它们之间是通过管道进行通信的。管道是一种两个进程间进行单向通信的机制。因为管道传递数据的单项性,管道又被称为半双工管道,管道的这一特点决定了其使用的局限性,其有以下特点:
- 数据只能由一个进程流向另一个进程(其中一个读管道,一个写管道);如果要进行双工通信,需要建立两个管道。
- 管道只能用于父子进程或者兄弟进程之间通信,也就是说管道只能用于具有亲缘关系的进程间通信。
例子:利用管道在父子进程间进行通信:
#include<unistd.h>
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#define INPUT 0
#define OUTPUT 1
int main()
{
int fd[2];
//定义子进程号
pid_t pid;
char buf[256];
int returned_count;
// 创建无名管道
pipe(fd);
//创建子进程
pid=fork();
if(pid<0)
{
printf("Error in fork\n");
exit(1);
}
else if(pid==0) // 执行子进程
{
printf("in the child process.....\n");
// 子进程向父进程写数据
close(fd[INPUT]);
write(fd[OUTPUT],"hello world",strlen("hello world"));
exit(0);
}
else // 执行父进程
{
printf("in the parent process....\n");
// 父进程从管道中读取数据,关闭管道写端
close(fd[OUTPUT]);
returned_count = read(fd[INPUT],buf,sizeof(buf));
printf("%d bytes of data received from child process: %s\n",returned_count,buf);
return 0;
}
在上面例子中,在子进程中写数据,父进程中读数据,两个进程之间实现了通信:父子进程分别拥有自己的读写通道,为了实现父子进程之间的读写,只需要把无关的读端或者写端的文件描述符关闭即可,上面就建立了一条“父进程写入子进程读取”
的通道,上述管道叫做无名管道。
还有一种管道叫做有名管道(named pipe 或 FIFO),它不同于无名管道之处在于它提供了一个路径名与之关联
,以FIFO的文件形式存在于文件系统中。这样即使与FIFO的创建进程不存在血缘关系,通过访问该路径,也能够彼此通过FIFO相互通信,因此,通过FIFO不相关的进程也能交换数据。
看一下示意图:
有名管道有以下特点:
- 它可以使互不相关的两个进程间进行彼此通信;
- 该管道可以通过路径名来指出,并且在文件系统中是可见的。在建立了管道之后,两个进程就可以把它当作普通文件一样进行读写操作,使用非常方便;
- FIFO严格地遵循先进先出的规则,对管道及FIFO的读操作总是从开始处返回数据,对它们的写操作则是把数据添加到末尾。
简单介绍以下其函数:
有名管道由mkfifo()函数创建,需要依赖的头文件是:
#include <sys/types.h>
#include <sys/stat.h>
mkfifo()函数原型是:
int mkfifo(const char* pathname,mode_t mode)
该函数第一个参数是普通的路径名,也就是创建后FIFO的名字,第二个参数与打开普通文件的open()函数中的mode参数相同。
小疑问?
能否使用有名管道让一个服务器和多个客户端进行双向交流?
解答:
事实上,一对多的形式经常出现,只要每次客户端向服务器发出的指令小于PIPE_BUF(管道写入最大值),它们就可以通过一个有名管道向服务器发送数据,并且客户端可以很容易的知道服务器传发数据的管道名。但问题是,不能用一个管道和所有客户打交道,如果不止一个客户在读同一个管道,则无法保证所有客户都能得到自己对应的回复。
一个解决办法就是每个客户在向服务器发送信息前都建立自己的读入管道,或让服务器在得到数据后再建立管道。使用客户的进程号(PID)作为管道名是一种常用方法。客户可以先把自己的进程号告诉服务器,然后到那个以自己进程号命名的管道中读取回复。
消息队列
消息队列用于运行于同一台机器上的进程间通信,它和管道非常相似,是一个再系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。
相关的函数有以下几个。
- 创建新消息队列取得已存在消息队列,函数原型是:
int msgget(key_t key,int msgflg);
参数中,key可以认为是一个端口号,也可以由函数ftok生成,msgflg如果等于IPC_CREAT,若没有该队列则创建一个并返回新标识符,若已存在则返回原标识符;msgflg如果等于IPC_EXCL,若没有该队列,则返回-1;若已经存在,则返回0;
- msgrcv函数从队列中取用消息:
ssize_t msgrcv(int msqid,void *msgp,size_t msgsz,long msgtyp,int msgflg);
- msgsnd将数据放到消息队列中:
int msgsnd(int msqid,const void* msgp,size_t msgsz,int msgflg);
共享内存
共享内存就是允许两个不相关的进程访问同一个逻辑内存,不同进程之间共享的内存通常安排在同一段物理内存中,进程可以将同一段共享内存连接到它们的自己的地址空间中,所有进程都可以访问共享内存中的地址,如果向共享内存中写入数据,所做的改动将立刻影响到可以访问同一段共享内存的其他进程。
不过!共享内存并未提供同步机制,也就是说,在第一个进程对共享内存的写财政哟结束之前,并无自动机制可以阻止第二个进程对它进行读取,所以通常需要用其他的机制来同步对共享内存的访问。
在Linux中提供了一组函数接口用于使用共享内存,首先常用的函数是shmget,该函数用来创建共享内存,它用到的头文件是:
#include<sys/shm.h>
函数原型是:
int shmget(key_t key,int size,int flag);
当共享内存创建成功后,其他进程可以通过调用shmat将其连接到自身的地址空间中,它的函数原型为:
void *shmat(int shmid,void* addr,int flag);
shmdt用于将共享内存从当前进程中分离,注意,将共享内存分离并不是删除它,只是使该共享内存对当前进程不再可用,它的原型如下:
int shmdt(const void *shmaddr);
信号量
在多线程的文章中已经说到过信号量,用于多线程的信号量是POSIX信号量,本文讲的是SYSTEM V 信号量,本质上说这两种都是用户态的进程可以使用的信号量。
在Liunx中提供了一组函数接口用于使用信号量,首先常用的函数semget。该函数用来创建和打开信号量,它的头文件是:
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
函数原型是:
int semget(key_t key,int nsems,int semflg);
ipcs命令
ipcs是一个UINX/Linux的命令,用于报告系统的消息队列、信号量、共享内存等。下面列举以下常用命令
ipcs -a
用于列出本用户所有相关的ipcs参数;
ipcs -q
用于列出进程中的消息队列;
ipcs -s
用于列出所有的信号量;
ipcs -m
用于列出所有的共享内存信息;
ipcs -l
用于列出系统的限额;
ipcs -q
用于列出最后的访问时间;
ipcs -q
用于列出当前的使用情况;