进程间通信就是在不同进程之间传播或交换信息,那么不同进程之间存在着什么双方都可以访问的介质呢?进程的用户空间是互相独立的,一般而言是不能互相访问的,唯一的例外是共享内存区。但是,系统空间却是“公共场所”,所以内核显然可以提供这样的条件。除此以外,那就是双方都可以访问的外设了。在这个意义上,两个进程当然也可以通过磁盘上的普通文件交换信息,或者通过“注册表”或其它数据库中的某些表项和记录交换信息。广义上这也是进程间通信的手段,但是一般都不把这算作“进程间通信”。因为那些通信手段的效率太低了,而人们对进程间通信的要求是要有一定的实时性。
Linux下进程通信的八种方法:管道(pipe),命名管道(FIFO),内存映射(mapped memeory),消息队列(message queue),共享内存(shared memory),信号量(semaphore),信号(signal),套接字(Socket).
(1) 管道(pipe):管道允许一个进程和另一个与它有共同祖先的进程之间进行通信;
管道是Linux支持的最初Unix IPC形式之一,具有以下特点:
管道是半双工的,数据只能向一个方向流动;需要双方通信时,需要建立起两个管道;
只能用于父子进程或者兄弟进程之间(具有亲缘关系的进程);
单独构成一种独立的文件系统:管道对于管道两端的进程而言,就是一个文件,但它不是普通的文件,它不属于某种文件系统,而是自立门户,单独构成一种文件系统,并且只存在与内存中。
数据的读出和写入:一个进程向管道中写的内容被管道另一端的进程读出。写入的内容每次都添加在管道缓冲区的末尾,并且每次都是从缓冲区的头部读出数据。
管道两端用描述字fd[0]以及fd[1]来描述,管道的两端是固定了任务的。fd[0]只能用于读,称其为管道读端;另一端则只能用于写,由描述字fd[1]来表示,称其为管道写端。
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <signal.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(void )
{
int pipefd[2];
pid_t pid;
//创建管道文件,并打开
if (pipe(pipefd) == -1 )
{
printf("pipe() err..\n");
return -1;
}
pid = fork();
if (pid == -1)
{
printf("fork err..\n");
return -1;
}
if (pid == 0)
{
close(pipefd[0]);
write(pipefd[1], "hello hello....", 10);
close(pipefd[1]);
printf("child .....quit\n");
}
else if (pid > 0 )
{
int len = 0;
char buf[100] = {0};
close(pipefd[1]);
len = read(pipefd[0], buf, 100);
printf("len:%d, buf:%s \n", len , buf);
close(pipefd[0]);
}
wait(NULL);
printf("parent ..quit\n");
return 0;
}
(2) 命名管道(FIFO):类似于管道,但是它可以用于任何两个进程之间的通信,命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建;
管道应用的一个重大缺陷就是没有名字,因此只能用于亲缘进程之间的通信。后来从管道为基础提出命名管道(named pipe,FIFO)的概念,该限制得到了克服。FIFO不同于管道之处在于它提供一个路径名与之关联,以FIFO的文件形式存在于文件系统中。这样,即使与FIFO的创建进程不存在亲缘关系的进程,只要可以访问该路径,就能够彼此通过FIFO相互通信(能够访问该路径的进程以及FIFO的创建进程之间),因此,通过FIFO不相关的进程也能交换数据。值得注意的是,FIFO严格遵循先进先出(first in first out),对管道及FIFO的读总是从开始处返回数据,对它们的写则把数据添加到末尾。它们不支持诸如lseek()等文件定位操作。
(3) 信号(signal):信号是比较复杂的通信方式,用于通知接收进程有某种事情发生,除了用于进程间通信外,进程还可以发送信号给进程本身;Linux除了支持UNIX早期信号语义函数signal外,还支持语义符合POSIX.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD即能实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数的功能);
在一个信号的生命周期中有两个阶段:生成和传送。当一个事件发生时,需要通知一个进程,这时生成一个信号。当进程识别出信号的到来,就采取适当的动作来传送或处理信号。在信号到来和进程对信号进行处理之间,信号在进程上挂起(pending)。
内核为进程生产信号,来响应不同的事件,这些事件就是信号源。主要的信号源如下: 异常:进程运行过程中出现异常; 其它进程:一个进程可以向另一个或一组进程发送信号; 终端中断:Ctrl-C,Ctrl-\等; 作业控制:前台、后台进程的管理; 分配额:CPU超时或文件大小突破限制; 通知:通知进程某事件发生,如I/O就绪等; 报警:计时器到期。在 Linux 中,信号的种类和数目与硬件平台有关。内核用一个字代表所有的信号,每个信号占一位,因此一个字的位数就是系统可以支持的最多信号种类数。
(4) 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它;
内存映射实际上是把文件映射到一块内存上,在进程中返回映射的地址,以后对该文件的操作就象操作内存一样,加快了文件/设备的访问速度。
与内存映射相关的另外一个概念就是共享内存,A,B进程共享内存的意思是将共享内存映射到A,B各自的进程地址空间中去,以后无论进程A或者是进程B对共享内存的读写,都彼此知道。
从功能上区分,内存映射是为了加快文件/设备的读写速度,而共享内存是加快多个进程间通信。
普通的进程读写文件,需要4次内核copy数据,而内存映射只需要2次,一次是把文件读到内存,另一次是把数据从内存写到文件中去。
(5) 消息队列(message queue): 消息队列是消息的连接表,包括POSIX消息对和System V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能成该无格式字节流以及缓冲区大小受限等缺点;
Linux的消息队列(queue)实质上是一个链表, 它有消息队列标识符(queue ID). msgget创建一个新队列或打开一个存在的队列; msgsnd向队列末端添加一条新消息; msgrcv从队列中取消息, 取消息是不一定遵循先进先出的, 也可以按消息的类型字段取消息.
消息队列也称为报文队列,消息队列是随内核持续的,只有在内核重起或显示删除一个消息队列时,该消息队列才会真正删除系统中记录消息队列的数据结构struct ipc_ids msg_ids位于内核中,系统中所有消息队列都可以在结构msg_ids中找到访问入口
消息队列其实就是一个消息的链表,每个消息队列有一个队列头,称为struct msg_queue,这个队列头描述了消息队列的key值,用户ID,组ID等信息,但它存于内核中而结构体struct msqid_ds能够返回或设置消息队列的信息,这个结构体位于用户空间中,与msg_queue结构相似
消息队列允许一个或多个进程向它写入或读取消息,消息队列是消息的链表。消息是按消息类型访问,进程必须指定消息类型来读取消息,同样,当向消息队列中写入消息时也必须给出消息的类型,如果读队列使用的消息类型为0,则读取队列中的第一条消息。
内核空间的结构体msg_queue描述了对应key值消息队列的情况,而对应于用户空间的msqid_ds这个结构体,因此,可以操作msgid_ds这个结构体来操作消息队列。
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/msg.h>
#include <fcntl.h>
//表示打开消息队列,如果消息队列不存在,那么报错。。。。
int main01()
{
int msgid;
//表示打开文件,这个文件必须要存在
msgid = msgget(0x1234, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
return 0;
}
int main02()
{
int msgid;
//表示打开消息对了,若存在,就使用旧
//若不存在则创建
msgid = msgget(0x1234, 0666 | IPC_CREAT);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("create message queue sucess...\n");
return 0;
}
//IPC_CREAT 和 IPC_EXCL 一起使用
//如果没有消息队列,则创建
//如果有了消息队列,则提示已经存在。。。
//若不存在则创建 。
//单独用IPC_EXCL没有意义
int main03()
{
int msgid;
msgid = msgget(0x1234, 0666 | IPC_CREAT | IPC_EXCL);
if (msgid == -1)
{
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
printf("create message queue sucess...\n");
return 0;
}
//IPC_PRIVATE 创建的消息队列,只在自己家族中使用,不在没有血缘关系的进程间用
//当我们使用了IPC_PRIVATE, IPC_CREAT | IPC_EXCL 不起作用
//IPC_PRIVATE 是一个宏 值为0
int main04()
{
int msgid;
//IPC_PRIVATE : 每一次创建的消息队列不一样
//叫私有的 言外之意是说。。。。。我的msgid即使传送给其他进程,其他进程也不用。。。。(血缘关系fork,除外)
msgid = msgget(IPC_PRIVATE, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue not exist\n");
}
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
printf("create Message Queues success...\n");
return 0;
}
int main()
{
int msgid;
msgid = msgget(0x1234, 0666 | IPC_CREAT );
if (msgid == -1)
{
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
printf("create message queue sucess...\n");
msgid = msgget(0x1234, 0666 );
if (msgid == -1)
{
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");//msgget err: Permission denied
return -1;
}
return 0;
}
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/msg.h>
//IPC对象。。消息队列是linux内核给你持久化(我们可以从linux内核中获取 获取消息队列的信息、修改消息队列)
/*
struct msqid_ds {
struct ipc_perm msg_perm; // Ownership and permissions
time_t msg_stime; // Time of last msgsnd() //
time_t msg_rtime; // Time of last msgrcv() //
time_t msg_ctime; // Time of last change //
unsigned long __msg_cbytes; // Current number of bytes in
queue (non-standard) //
msgqnum_t msg_qnum; // Current number of messages
in queue //
msglen_t msg_qbytes; // Maximum number of bytes
allowed in queue //
pid_t msg_lspid; // PID of last msgsnd() //
pid_t msg_lrpid; // PID of last msgrcv() //
};
The ipc_perm structure is defined in <sys/ipc.h> as follows (the highlighted fields are settable using
IPC_SET):
struct ipc_perm {
key_t key; // Key supplied to msgget() //
uid_t uid; // Effective UID of owner //
gid_t gid; // Effective GID of owner //
uid_t cuid; // Effective UID of creator //
gid_t cgid; // Effective GID of creator //
unsigned short mode; // Permissions //
unsigned short seq; // Sequence number //
};
*/
// int msgctl(int msqid, int cmd, struct msqid_ds *buf);
int main01()
{
int msgid;
int ret = 0;
msgid = msgget(0x2234, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue not exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
struct msqid_ds buf;
memset(&buf, 0, sizeof( struct msqid_ds ));
/*
IPC_STAT:
Copy information from the kernel data structure associated with msqid into the msqid_ds structure pointed to by buf.
The caller must have read permission on the message queue.
*/
ret = msgctl(msgid, IPC_STAT, &buf);
if (ret == -1)
{
perror("msgget err");
return -1;
}
printf("buf.msg_perm.mode %o \n", buf.msg_perm.mode);
printf("buf.__msg_cbytes %lu \n", buf.__msg_cbytes);
printf("buf.msg_qnum %lu \n", buf.msg_qnum);
return 0;
}
int main()
{
int msgid;
int ret = 0;
//rwx
msgid = msgget(0x2234, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue not exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
struct msqid_ds buf;
memset(&buf, 0, sizeof( struct msqid_ds ));
ret = msgctl(msgid, IPC_STAT, &buf);
if (ret == -1)
{
perror("msgget err");
return -1;
}
/*
printf("权限信息 %o \n", buf.msg_perm.mode);
printf("当前消息队列中有多少字节 %ld \n", buf.__msg_cbytes);
printf("当前消息队列的个数 %d \n", buf.msg_qnum);
*/
buf.msg_perm.mode = 0644;
ret = msgctl(msgid, IPC_SET, &buf);
if (ret == -1)
{
perror("msgget err");
return -1;
}
ret = msgctl(msgid, IPC_RMID, NULL);
if (ret == -1)
{
perror("msgget err");
return -1;
}
else
{
printf("IPC_RMID success\n");
}
return 0;
}
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/msg.h>
//IPC对象。。消息队列是linux内核给你持久化(我们可以从linux内核中获取 获取消息队列的信息、修改消息队列)
int main41()
{
printf("sizeof(long):%d \n", sizeof(long));
printf("sizeof(int):%d \n", sizeof(int));
return 0;
}
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[1024*10]; /* message data */
};
int main(int argc, char *argv[])
{
int msgid;
int ret = 0;
if (argc != 3)
{
fprintf(stderr, "Usage: %s <messageType type> <MessageLength bytes> \n", argv[0]);
exit(EXIT_FAILURE);
}
int type = atoi(argv[1]);
int len = atoi(argv[2]);
//rwx
msgid = msgget(0x1234, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue not exist\n");
}
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
struct msgbuf buf;
memset(&buf, 0, sizeof(struct msgbuf));
buf.mtype = type;
strcpy(buf.mtext, "1234567890123456");
ret = msgsnd(msgid, &buf, len, IPC_NOWAIT);
if (ret < 0)
{
perror("msgsnd err");
return -1;
}
return 0;
}
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/msg.h>
//IPC对象。。消息队列是linux内核给你持久化(我们可以从linux内核中获取 获取消息队列的信息、修改消息队列)
#define MsgMax 1024*10
struct msgbuf {
long mtype; /* message type, must be > 0 */
char mtext[MsgMax]; /* message data */
};
int main01()
{
int msgid;
int ret = 0;
int flag = 0;
int type = 0;
msgid = msgget(0x1234, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue not exist\n");
}
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
struct msgbuf buf;
memset(&buf, 0, sizeof(struct msgbuf));
/*
ssize_t msgrcv(int msqid, void *msgp, size_t msgsz, long msgtyp,int msgflg);
*/
// If msgtyp is 0, then the first message in the queue is read.
//ret = msgrcv(msgid, &buf, MsgMax, 0, IPC_NOWAIT); // 没有消息立马返回出错
ret = msgrcv(msgid, &buf, MsgMax, 0, 0);//没有消息将被Linux内核block,收到消息后立马返回
if (ret < 0)
{
perror("msgsnd err");
return -1;
}
buf.mtext[ret] = '\0';
printf("mtext:%s \n", buf.mtext);
return 0;
}
//按照类型去读数据
int main(int argc, char *argv[])
{
int msgid;
int ret = 0;
int flag = 0;
int type = 0;
if (argc == 1)
{
fprintf(stderr, "Usage: %s <messageType 1 2 3> <n block > \n", argv[0]);
exit(EXIT_FAILURE);
}
if (argc == 2)
{
type = atoi(argv[1]);
flag = 0;
}
if (argc == 3)
{
type = atoi(argv[1]);
flag = flag | IPC_NOWAIT;
}
msgid = msgget(0x1234, 0666);
if (msgid == -1)
{
if (errno == ENOENT)
{
printf("message queue not exist\n");
}
if (errno == EEXIST)
{
printf("message queue exist\n");
}
perror("msgget err");
return -1;
}
printf("msgid:%d \n", msgid);
struct msgbuf buf;
memset(&buf, 0, sizeof(struct msgbuf));
ret = msgrcv(msgid, &buf, MsgMax, type, flag);
if (ret < 0)
{
perror("msgsnd err");
return -1;
}
buf.mtext[ret] = '\0';
printf("mtext:%s \n", buf.mtext);
return 0;
}
(6) 信号量(semaphore):信号量主要作为进程间以及同进程不同线程之间的同步手段;
信号量及信号量上的P,V操作是E.W.Dijkstra 在1965年提出的一种解决同步、互斥问题的较通用的方法,并在很多操作系统中得以实现, Linux改进并实现了这种机制。
信号量(semaphore )实际是一个整数,它的值由多个进程进行测试(test)和设置(set)。就每个进程所关心的测试和设置操作而言,这两个操作是不可中断的,或称“原子”操作,即一旦开始直到两个操作全部完成。测试和设置操作的结果是:信号量的当前值和设置值相加,其和或者是正或者为负。根据测试和设置操作的结果,一个进程可能必须睡眠,直到有另一个进程改变信号量的值。
信号量可用来实现所谓的“临界区”的互斥使用,临界区指同一时刻只能有一个进程执行其中代码的代码段。为了进一步理解信号量的使用,下面我们举例说明。
假设你有很多相互协作的进程,它们正在读或写一个数据文件中的记录。你可能希望严格协调对这个文件的存取,于是你使用初始值为1的信号量,在这个信号量上实施两个操作,首先测试并且给信号量的值减1,然后测试并给信号量的值加1。当第一个进程存取文件时,它把信号量的值减1,并获得成功,信号量的值现在变为0,这个进程可以继续执行并存取数据文件。但是,如果另外一个进程也希望存取这个文件,那么它也把信号量的值减1,结果是不能存取这个文件,因为信号量的值变为-1。这个进程将被挂起,直到第一个进程完成对数据文件的存取。当第一个进程完成对数据文件的存取,它将增加信号量的值,使它重新变为1,现在,等待的进程被唤醒,它对信号量的减1操作将获得成功。
上述的进程互斥问题,是针对进程之间要共享一个临界资源而言的,信号量的初值为1。实际上,信号量作为资源计数器,它的初值可以是任何正整数,其初值不一定为0或1。另外,如果一个进程要先获得两个或多个的共享资源后才能执行的话,那么,相应地也需要多个信号量,而多个进程要分别获得多个临界资源后方能运行,这就是信号量集合机制,Linux 讨论的就是信号量集合问题。
(7) 共享内存 (shared memory):它使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。这是针对其他通信机制运行效率较低而设计的。它往往与其他通信机制,如信号量结合使用,以达到进程间的同步及互斥;
分配一个新的共享内存块会创建新的内存页面。因为所有进程都希望共享对同一块内存的访问,只应由一个进程创建一块新的共享内存。再次分配一块已经存在的内存块不会创建新的页面,而只是会返回一个标识该内存块的标识符。一个进程如需使用这个共享内存块,则首先需要将它绑定到自己的地址空间中。这样会创建一个从进程本身虚拟地址到共享页面的映射关系。当对共享内存的使用结束之后,这个映射关系将被删除。当再也没有进程需要使用这个共享内存块的时候,必须有一个(且只能是一个)进程负责释放这个被共享的内存页面。
共享内存是指把共享数据放到共享内存区域,任何需要访问共享内存区域数据的进程都在自己的进程地址空间中开辟一个新的内存区域,用来映射共享内存数据的物理页面,所有需要访问共享内存区域的进程都要把该共享区域映射到本进程的地址空间中去,系统用shmget获得或创建一个IPC的共享内存区域,并返回相应的标识符,通过shmat将共享内存区域映射到进程的地址空间中去,每一个共享内存区域都对应shm文件系统上的一个文件,相当于映射shm文件系统上的同名文件到共享内存区域,shmdt是解除对共享内存区的映射,shmctl是对共享内存区的控制操作。
共享内存作用是加快进程间的通信,共享内存的修改对进程是可见的,将共享内存区域映射到进程地址空间中去,而内存映射是加快进程访问文件/设备的速度
(1)系统V共享内存,不把数据写入磁盘,而mmap()映射普通文件可以指定何时把数据写入磁盘
V共享内存是通过特殊的文件系统shm中的文件实现的,文件系统shm的安装点在交换区上,重新引导后,数据会丢失
(2)系统V共享内存是随内核持续的,所有访问共享内存的进程都已经终止,它仍然存在,对内核引导前,对该共享内存区域的任何改写操作一直保留
(3)mmap()映射普通文件是随进程持续的,一定要注意何时终止进程
(8) 套接字(Socket):它是更为通用的进程间通信机制,可用于不同机器之间的进程间通信。起初是由UNIX系统的BSD分支开发出来的,但现在一般可以移植到其他类UNIX系统上:Linux和System V的变种都支持套接字;