一、介绍
进程之间是相对独立的,不能直接访问彼此的资源,然而在逻辑上进程往往不是孤立的,彼此间需要进行数据交换——进程通信。进程间通信(IPC——Inter Process Communication )包括以下几种方式:
(1)管道(Pipeline)
管道是Linux最初支持的IPC方式,可分为无名管道, 命名管道。在管道通信中,发送进程以字符流形式将大量数据送入管道,接收进程可从管道接收数据,从而实现通信。
(2)信号量 (Semaphore)
信号量是一种被保护的变量,只能通过初始化和两个标准的原子操作即P 、 V 操作(也称为wait 、 signal操作)来访问。信号量用来实现进程/线程间的同步。
(3)信号(Signal)
信号是Unix系统最早的 IPC 方式之一。操作系统通过信号通知某一进程发生了某种预定义的事件;接收到信号的进程可以选择不同的方式处理该信号,一是可以采用默认处理机制——进程中断或退出,一是忽略该信号,也可以自定义该信号的处理函数。
内核为进程生产信号以响应不同的事件,这些事件就是信号源。信号源可以是:异常、其他进程、终端的中断、 cpu 超时、文件过大等、内核通知等等。
(4)消息队列(Message Queue)
消息队列就是消息的一个链表,它允许一个或者多个进程向它写消息,一个或多个进程从中读消息。 Linux 维护了一个消息队列向量表: msgqueue ,来表示系统中所有的消息队列。消息队列通信克服了信号传递信息少的弱点。
(5)共享内存(shared memory)
共享内存是一个可被多个进程访问的内存区域,由一个进程所创建,其他进程可以挂载到该共享内存中,从而实现进程间通信。共享内存是最快的IPC机制,但由于linux本身不能实现对其同步控制,需要用户程序进行并发访问控制(例如信号量机制)。
(6)套接字(socket)
套接字可以实现不同主机间的进程通信。一个套接字是进程间通信的端点(endpoint),其他进程可以访问、连接和进行数据通信。
二、相关函数和注意事项
(1)管道
①无名管道
#include <unistd.h>
int pipe(int pipedes[2]);
参数:
pipedes:文件描述符数组,0读1写
返回值:成功返回0,错误返回-1(置errno)
说明:创建一个管道。pipedes[1]为写文件描述符,pipedes[0]为读文件描述符
对管道读写用read,write。
- popen 函数也可以创建一个无名管道,但同时还会创建子进程、启动shell 。故popen是对管道的一种封装调用,pipe则是一个底层调用。
- 管道可以看做是一种特殊的文件。其特殊性表现在,首先它只存在于内存中,其次管道中的数据从读端读去后就被移出管道,也就是从管道使用的内核缓冲区中移除。
- 管道容量:在Linux内核2.6.11版本之前,管道的容量,或者说一个管道使用的内核缓冲区大小,与系统的页面大小相同,在 i386 架构下就是 4096 字节,这与 POSIX 标准一致。在Linux 内核3.13.0 版本中管道容量可以根据用户的需求动态增长,最大为 1048576 字节。root 用户可以通过设置 /proc/sys/fs/pipe-max-size 参数来改变该值。
- 管道的读写行为和一般文件也不相同。当一个进程使用read读取一个空管道时,read 将会阻塞,直到管道中有数据被写入;当一个进程试图向一个满的管道写入数据时,write将会被阻塞,直到足够多的数据被从管道中读取, write 可以将数据全部写入管道中。根据 POSIX.1 2001标准,当向管道中写入的数据量小于管道容量时,写入过程是原子性的,即:写入到管道中的数据是一个连续的流;当向管道中写入的数据量大于管道容量时,写入过程就不一定是原子性的,即:该进程写入到管道中的数据可能会与其他进程写入到管道中的数据交织在一起。
②命名管道
无名管道可以比较方便地在相关进程之间实现数据通信,在不相关进程之间通信可以使用命名管道。 命名管道也被称为FIFO文件,它是一种特殊类型的文件。虽然创建方式不同,但命名管道和无名管道的IO行为都是相同的。
#include <sys/stat.h>
int mkfifo(const char *path, mode_t perms);
参数:
path:有名管道的路径名
perms:管道的访问权限,有O_RDONLY(只读)、O_WRONLY(只写)、O_NONBLOCK(非阻塞);。
返回值:成功返回0,错误返回-1(置errno)
- 有名管道的名字可以在目录树中找到,但占用的是内存空间,不拥有磁盘块。
- 对于管道的访问权限,虽然Linux 3.13.0内核支持以O_RDWR 模式打开一个命名管道,但是 POSIX 对此没有明确定义,而且从实践上来看以O_RDWR 来使用一个命名管道很容易出现错误。故建议以只读或只写的方式打开命名管道,实现单向数据传输。
(2)信号量
一般情况,多个进程在访问共享对象时使用信号量实现同步操作,如经典的生产者/消费者问题。
- 实质是一个整数计数器,记录可同时访问共享资源的单元个数。
- 当进程要求使用某资源,先对信号量减1
- >=0:进程可以用该资源
- <0:进程休眠,直至信号量值大于或等于0时被唤醒。
- 进程对资源访问结束时,信号量值加1
- <=0:证明有其他进程等待,唤醒队列第一个进程。
- POSIX信号量只针对一个信号量操作,而SystemV信号量可以对一个信号量集合操作。
#include <sys/types.h>
#include <sys/ipc.h>
int semget(key_t key, int nsems, int semflg);
功能:创建信号量
参数:
key:信号量的键值
nsems:信号量的个数
semflg:创建标志,可以是IPC_CREAT或IPC_CREAT | IPC_EXCL
返回值:成功返回共享内存ID;失败返回-1,并设置errno
-----
int semctl(int semid, int semnum, int cmd, ...);
功能:信号量控制
参数:
semid:信号量ID
cmd:控制命令
semnum:信号量集中信号量的序号
返回值:成功返回0,失败返回-1
(3)信号
#include <signal.h>
void (*signal(int signum, void ((*handler)(int))))(int);
功能:指定signum信号的处理函数为handler。
参数:
signum:信号值,可用kill -l查看
handler:
执行系统默认动作:SIG_DFL
忽略:SIG_IGN 执行忽略
捕捉:参数为int返回值为void的信号处理函数。
返回值:成功上次对该信号的处理函数地址,失败返回SIG_ERR
注意:SIGKILL与SIGSTOP不可忽略与捕捉
-----
#include <signal.h>
int kill(pid_t pid, int signum);
功能:向pid进程发送信号signum
参数:
pid>0:发给pid进程
pid==0:发给与发送进程同一进程组的所有进程。
pid<0:发给进程组号为|pid|的所有进程,且发送进程对该进程具有发送信号权限。
pid==-1:发给所有进程,且发送进程对该进程具有发送信号权限。(各种内核版本说法不一)
返回值:成功返回0,错误返回-1。
-----
int raise(int sig);
功能:给当前进程发送信号
参数:
sig:要发送的信号
返回值:成功是返回0;失败时返回非0值
raise(sig);等价于kill(getpid(), sig);
(4)消息队列
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>
int msgget(key_t key, int msgflg);
功能:创建、打开消息队列
参数:
key:消息队列键值
msgflg:创建方式
返回值:成功返回消息队列ID;失败返回-1
-----
int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg)
功能:发送消息
参数:
msgid:消息队列ID
msgp:指向消息指针
msgsz:消息长度
msgflg:发送方式
返回值:成功返回消息队列ID;失败返回-1
-----
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp, int msgflg);
功能:接收消息
参数:
msgid:消息队列ID
msgp:指向消息的指针
msgsz:消息长度
msgflg:接收方式
返回值:成功返回消息正文字节数;失败返回-1
-----
int msgctl(int msqid, int cmd, struct msqid_ds *buf);
功能:控制消息队列的行为
参数:
msgid:消息队列ID
cmd:控制命令
返回值:成功返回0;失败返回-1
- 消息队列在系统重启前一直有效(随内核持续),克服信号最多在接收信号进程的生命周期中有效(随进程持续)的缺点。
- 消息队列,即消息的一个链表,一系列连续排列的消息,保存在内核中,通过消息队列的引用标识符访问。每个消息都包括两部分:类型和正文。
- 队列,先进先出。如果类型相同,先发送的消息将先被读出。
- 当消息队列满时的写操作或消息队列空时的读操作都会阻塞。
(5)共享内存
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flags);
功能:创建并得到共享内存段
参数:
key:共享内存关联的key,可为IPC_PRIVATE(类似无名管道,亲缘关系进程使用)或ftok生成
size:共享内存大小
shmflg:创建标志(权限)
返回值:成功返回共享内存标识符,错误返回-1(置errno)
-----
int shmctl(int shmid, int cmd, struct shmid_ds *data);
功能:控制共享内存段(包含删除)
参数:
shmid:要操作的共享内存的id
cmd:要执行的操作,可选操作有IPC_STAT、IPC_SET、IPC_RMID
IPC_STAT:该命令用来获取消息队列信息,返回的信息存贮在buf指向的msqid结构中;
IPC_SET:该命令用来设置消息队列的属性,要设置的属性存储在buf指向的msqid结构中
IPC_RMID:删除msqid标识的消息队列;
buf:保存或设置共享内存属性的地址
返回值:成功返回0,错误返回-1(置errno)
-----
void *shmat(int shmid, const void *shmaddr, int flags);
功能:连接共享内存段
参数:
shmid:共享内存标识符。
shmaddr:进程映射内存段的地址,一般设置为NULL,表示由系统安排
flags:对该内存段是否设置只读,默认设置读写。
返回值:成功返回进程中该内存段的地址,错误返回-1(置errno)
-----
int shmdt(const void *shmaddr);
功能:撤销共享内存段的映射
返回值:成功返回0,错误返回-1(置errno)
- 共享内存是一种最为高效的进程间通信方式,在内核空间创建,可被进程映射到用户空间访问。
- 由于多个进程可同时访问共享内存,因此需要同步和互斥机制配合使用
-
每块共享内存大小有限制,查看三个IPC通信的属性:ipcs -l。
(6)套接字
套接字不仅可用于本地通信,更可以用于网络通信,即不同计算机上的进程间通信。在进行通信的两个进程中,主动发起通信请求的一方称为客户 ,被动响应的一方称为服务器 。它们既可以是处在同一台计算机上的两个进程,也可以分别处于网络环境下的不同主机上。这种通信模型叫作Client/Server 模型(简称 C/S 模型 ),是所有网络应用的基础。
#include <sys/socket.h>
int socket (int family, int type, int protocol);
功能:创建套接字。
参数:
family :程序所在主机采用的通信协议,AF_INET(IPv4),AF_INET6(IPv6)。
type:要建立的套接字类型:SOCK_STREAM(流式),SOCK_DGRAM(数据报),SOCK_RAW(原始)。
protocol::一般为0,除原始套接字外。
返回值:成功返回套接字描述符,失败返回-1。
-----
int bind(int sockfd, const struct sockaddr *addr, socklen_len len)
功能:指明套接字将使用本地的哪一个协议端口进行数据传送(IP地址和端口号)
参数:
sockfd:由socket()调用返回的套接字描述符。
addr :本地套接字地址,通用地址结构
len :本地套接字地址长度,即addr的长度。
返回值:成功返回0,失败返回-1并置errno。
-----
setsockopt( sockfd, SOL_SOCKET, SO_REUSEADDR, (char*)&val, sizeof(val));
功能:强制关闭服务器时,会出现TIME_WAIT状态,避免出现地址已经使用的错误
-----
int listen(int sockfd, int backlog);
功能:服务器在已绑定的端口上开始监听,将sockfd转换成为监听套接字。
参数:
sockfd:由socket()调用返回的套接字描述符。
backlog:请求队列大小,已经收到连接请求但还没accept的队列。
返回值:成功返回0,失败返回-1。
-----
int accept( int sockfd, struct sockaddr * client, int * addrlen);
功能:从连接请求队列中接受一个连接,sockfd作为监听套接字继续监听。
参数:
client :请求连接主机的地址。
addrlen :请求连接主机地址长度。
返回值:成功返回连接套接字描述符,失败返回-1。
-----
int recv(int sockfd,void *buf,int len,unsigned int flags);
功能:接收数据,比read多了一个flags
参数:
flags:传输控制标志。
0:同read()
MSG_PEEK:只查看数据而不读出数据,后续读操作仍然能读出所查看的该数据;
MSG_OOB:忽略常规数据,只读带外(紧急)数据。
MSG_WAITALL:等希望接收的数据字节到达后返回,否则阻塞等待。
-----
int send(int sockfd, void *buf,int len,int flags);
功能:发送数据,比write多了一个flags
参数:
flags:传输控制标志。
0:同write()
MSG_OOB:发送带外(紧急)数据。
MSG_DONTROUTE:忽略底层协议的路由设置,只能将数据发送给与发送机处在同一个网络中的机器上。
-----
int close(int sockfd);
功能:套接字描述符将不再可用,系统会将套接字发送缓冲区的数据发送出去,直到TCP连接终止。
返回值:成功返回0;失败返回-1。
-----
int connect( int sockfd, struct sockaddr * addr,int addrlen);
功能:客户端与服务器建立连接,激发TCP的三路握手过程。
参数:
addr:目的主机地址,即服务器的地址。
返回值:成功返回0,失败返回-1。