2016-02-26 15:20:12 qq_17242957 阅读数 598

进程间的通信方式:

   1.管道(pipe)及有名管道(named pipe):

     管道可用于具有亲缘关系进程间的通信,有名管道除了具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。

   2.信号(signal):

     信号是在软件层次上对中断机制的一种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,一个进程收到一个信号与处理器收到一个中断请求效果上可以说是一致得。


   3.消息队列(message queue):

     消息队列是消息的链接表,它克服了上两种通信方式中信号量有限的缺点,具有写权限得进程可以按照一定得规则向消息队列中添加新信息;对消息队列有读权限得进程则可以从消息队列中读取信息。       

      消息缓冲通信技术是由Hansen首先提出的,其基本思想是:根据”生产者-消费者”原理,利用内存中公用消息缓冲区实现进程之间的信息交换.  
   
              内存中开辟了若干消息缓冲区,用以存放消息.每当一个进程向另一个进程发送消息时,便申请一个消息缓冲区,并把已准备好的消息送到缓冲区,然后把该消息缓冲区插入到接收进程的消息队列中,最后通知接收进程.接收进程收到发送里程发来的通知后,从本进程的消息队列中摘下一消息缓冲区,取出所需的信息,然后把消息缓冲区不定期给系统.系统负责管理公用消息缓冲区以及消息的传递.  
   
              一个进程可以给若干个进程发送消息,反之,一个进程可以接收不同进程发来的消息.显然,进程中关于消息队列的操作是临界区.当发送进程正往接收进程的消息队列中添加一条消息时,接收进程不能同时从该消息队列中到出消息:反之也一样.  
   
  消息缓冲区通信机制包含以下列内容:  
   
  (1)           消息缓冲区,这是一个由以下几项组成的数据结构:  
  1、       消息长度  
  2、       消息正文  
  3、       发送者  
  4、       消息队列指针  
   
  (2)消息队列首指针m-q,一般保存在PCB中。  
  (1)   互斥信号量m-mutex,初值为1,用于互斥访问消息队列,在PCB中设置。  
  (2)   同步信号量m-syn,初值为0,用于消息计数,在PCB中设置。  
  (3)   发送消息原语send  
  (4)   接收消息原语receive(a)  

 

   4.共享内存(shared memory):

     可以说这是最有用的进程间通信方式。它使得多个进程可以访问同一块内存空间,不同进程可以及时看到对方进程中对共享内存中数据得更新。这种方式需要依靠某种同步操作,如互斥锁和信号量等。

  这种通信模式需要解决两个问题:第一个问题是怎样提供共享内存;第二个是公共内存的互斥关系则是程序开发人员的责任。   

   5.信号量(semaphore):

     主要作为进程之间及同一种进程的不同线程之间得同步和互斥手段。

 

   6.套接字(socket);

     这是一种更为一般得进程间通信机制,它可用于网络中不同机器之间的进程间通信,应用非常广泛。

 

http://blog.csdn.net/eroswang/archive/2007/09/04/1772350.aspx

linux下的进程间通信-详解

详细的讲述进程间通信在这里绝对是不可能的事情,而且笔者很难有信心说自己对这一部分内容的认识达到了什么样的地步,所以在这一节的开头首先向大家推荐著 名作者Richard Stevens的著名作品:《Advanced Programming in the UNIX Environment》,它的中文译本《UNIX环境高级编程》已有机械工业出版社出版,原文精彩,译文同样地道,如果你的确对在Linux下编程有浓 厚的兴趣,那么赶紧将这本书摆到你的书桌上或计算机旁边来。说这么多实在是难抑心中的景仰之情,言归正传,在这一节里,我们将介绍进程间通信最最初步和最 最简单的一些知识和概念。 
   首先,进程间通信至少可以通过传送打开文件来实现,不同的进程通过一个或多个文件来传递信息,事实上,在很多应用系统里,都使用了这种方法。但一般说来, 进程间通信(IPC:InterProcess Communication)不包括这种似乎比较低级的通信方法。Unix系统中实现进程间通信的方法很多,而且不幸的是,极少方法能在所有的Unix系 统中进行移植(唯一一种是半双工的管道,这也是最原始的一种通信方式)。而Linux作为一种新兴的操作系统,几乎支持所有的Unix下常用的进程间通信 方法:管道、消息队列、共享内存、信号量、套接口等等。下面我们将逐一介绍。 

   2.3.1 管道 
   管道是进程间通信中最古老的方式,它包括无名管道和有名管道两种,前者用于父进程和子进程间的通信,后者用于运行于同一台机器上的任意两个进程间的通信。 
   无名管道由pipe()函数创建: 
   #include <unistd.h> 
   int pipe(int filedis[2]); 
   参数filedis返回两个文件描述符:filedes[0]为读而打开,filedes[1]为写而打开。filedes[1]的输出是filedes[0]的输入。下面的例子示范了如何在父进程和子进程间实现通信。 

#define INPUT 0 
#define OUTPUT 1 

void main() { 
int file_descriptors[2]; 
/*定义子进程号 */ 
pid_t pid; 
char buf[256]; 
int returned_count; 
/*创建无名管道*/ 
pipe(file_descriptors); 
/*创建子进程*/ 
if((pid = fork()) == -1) { 
printf("Error in fork\n"); 
exit(1); 

/*执行子进程*/ 
if(pid == 0) { 
printf("in the spawned (child) process...\n"); 
/*子进程向父进程写数据,关闭管道的读端*/ 
close(file_descriptors[INPUT]); 
write(file_descriptors[OUTPUT], "test data", strlen("test data")); 
exit(0); 
} else { 
/*执行父进程*/ 
printf("in the spawning (parent) process...\n"); 
/*父进程从管道读取子进程写的数据,关闭管道的写端*/ 
close(file_descriptors[OUTPUT]); 
returned_count = read(file_descriptors[INPUT], buf, sizeof(buf)); 
printf("%d bytes of data received from spawned process: %s\n", 
returned_count, buf); 


   在Linux系统下,有名管道可由两种方式创建:命令行方式mknod系统调用和函数mkfifo。下面的两种途径都在当前目录下生成了一个名为myfifo的有名管道: 
     方式一:mkfifo("myfifo","rw"); 
     方式二:mknod myfifo p 
   生成了有名管道后,就可以使用一般的文件I/O函数如open、close、read、write等来对它进行操作。下面即是一个简单的例子,假设我们已经创建了一个名为myfifo的有名管道。 
  /* 进程一:读有名管道*/ 
#include <stdio.h> 
#include <unistd.h> 
void main() { 
FILE * in_file; 
int count = 1; 
char buf[80]; 
in_file = fopen("mypipe", "r"); 
if (in_file == NULL) { 
printf("Error in fdopen.\n"); 
exit(1); 

while ((count = fread(buf, 1, 80, in_file)) > 0) 
printf("received from pipe: %s\n", buf); 
fclose(in_file); 

  /* 进程二:写有名管道*/ 
#include <stdio.h> 
#include <unistd.h> 
void main() { 
FILE * out_file; 
int count = 1; 
char buf[80]; 
out_file = fopen("mypipe", "w"); 
if (out_file == NULL) { 
printf("Error opening pipe."); 
exit(1); 

sprintf(buf,"this is test data for the named pipe example\n"); 
fwrite(buf, 1, 80, out_file); 
fclose(out_file); 


   2.3.2 消息队列 
   消息队列用于运行于同一台机器上的进程间通信,它和管道很相似,是一个在系统内核中用来保存消息的队列,它在系统内核中是以消息链表的形式出现。消息链表中节点的结构用msg声明。
事实上,它是一种正逐渐被淘汰的通信方式,我们可以用流管道或者套接口的方式来取代它,所以,我们对此方式也不再解释,也建议读者忽略这种方式。 

   2.3.3 共享内存 
    共享内存是运行在同一台机器上的进程间通信最快的方式,因为数据不需要在不同的进程间复制。通常由一个进程创建一块共享内存区,其余进程对这块内存区进行 读写。得到共享内存有两种方式:映射/dev/mem设备和内存映像文件。前一种方式不给系统带来额外的开销,但在现实中并不常用,因为它控制存取的将是 实际的物理内存,在Linux系统下,这只有通过限制Linux系统存取的内存才可以做到,这当然不太实际。常用的方式是通过shmXXX函数族来实现利 用共享内存进行存储的。 
   首先要用的函数是shmget,它获得一个共享存储标识符。 

     #include <sys/types.h> 
     #include <sys/ipc.h> 
     #include <sys/shm.h> 

      int shmget(key_t key, int size, int flag); 
    这个函数有点类似大家熟悉的malloc函数,系统按照请求分配size大小的内存用作共享内存。Linux系统内核中每个IPC结构都有的一个非负整数 的标识符,这样对一个消息队列发送消息时只要引用标识符就可以了。这个标识符是内核由IPC结构的关键字得到的,这个关键字,就是上面第一个函数的 key。数据类型key_t是在头文件sys/types.h中定义的,它是一个长整形的数据。在我们后面的章节中,还会碰到这个关键字。 
   
     当共享内存创建后,其余进程可以调用shmat()将其连接到自身的地址空间中。 
   void *shmat(int shmid, void *addr, int flag); 
   shmid为shmget函数返回的共享存储标识符,addr和flag参数决定了以什么方式来确定连接的地址,函数的返回值即是该进程数据段所连接的实际地址,进程可以对此进程进行读写操作。 
    使用共享存储来实现进程间通信的注意点是对数据存取的同步,必须确保当一个进程去读取数据时,它所想要的数据已经写好了。通常,信号量被要来实现对共享存 储数据存取的同步,另外,可以通过使用shmctl函数设置共享存储内存的某些标志位如SHM_LOCK、SHM_UNLOCK等来实现。 

   2.3.4 信号量 
   信号量又称为信号灯,它是用来协调不同进程间的数据对象的,而最主要的应用是前一节的共享内存方式的进程间通信。本质上,信号量是一个计数器,它用来记录对某个资源(如共享内存)的存取状况。一般说来,为了获得共享资源,进程需要执行下列操作: 
   (1) 测试控制该资源的信号量。 
   (2) 若此信号量的值为正,则允许进行使用该资源。进程将信号量减1。 
   (3) 若此信号量为0,则该资源目前不可用,进程进入睡眠状态,直至信号量值大于0,进程被唤醒,转入步骤(1)。 
   (4) 当进程不再使用一个信号量控制的资源时,信号量值加1。如果此时有进程正在睡眠等待此信号量,则唤醒此进程。 
    维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件/usr/src/linux/include /linux /sem.h 中看到内核用来维护信号量状态的各个结构的定义。信号量是一个数据集合,用户可以单独使用这一集合的每个元素。要调用的第一个函数是semget,用以获 得一个信号量ID。 

struct sem {
  short sempid;/* pid of last operaton */
  ushort semval;/* current value */
  ushort semncnt;/* num procs awaiting increase in semval */
  ushort semzcnt;/* num procs awaiting semval = 0 */
}

   #include <sys/types.h> 
   #include <sys/ipc.h> 
   #include <sys/sem.h> 
   int semget(key_t key, int nsems, int flag); 

   key是前面讲过的IPC结构的关键字,flag将来决定是创建新的信号量集合,还是引用一个现有的信号量集合。nsems是该集合中的信号量数。如果是创建新 集合(一般在服务器中),则必须指定nsems;如果是引用一个现有的信号量集合(一般在客户机中)则将nsems指定为0。 

   semctl函数用来对信号量进行操作。 
   int semctl(int semid, int semnum, int cmd, union semun arg); 
   不同的操作是通过cmd参数来实现的,在头文件sem.h中定义了7种不同的操作,实际编程时可以参照使用。 
   
     semop函数自动执行信号量集合上的操作数组。 
   int semop(int semid, struct sembuf semoparray[], size_t nops); 
   semoparray是一个指针,它指向一个信号量操作数组。nops规定该数组中操作的数量。 

   下面,我们看一个具体的例子,它创建一个特定的IPC结构的关键字和一个信号量,建立此信号量的索引,修改索引指向的信号量的值,最后我们清除信号量。在下面的代码中,函数ftok生成我们上文所说的唯一的IPC关键字。 

#include <stdio.h> 
#include <sys/types.h> 
#include <sys/sem.h> 
#include <sys/ipc.h> 
void main() { 
key_t unique_key; /* 定义一个IPC关键字*/ 
int id; 
struct sembuf lock_it; 
union semun options; 
int i; 

unique_key = ftok(".", 'a'); /* 生成关键字,字符'a'是一个随机种子*/ 
/* 创建一个新的信号量集合*/ 
id = semget(unique_key, 1, IPC_CREAT | IPC_EXCL | 0666); 
printf("semaphore id=%d\n", id); 
options.val = 1; /*设置变量值*/ 
semctl(id, 0, SETVAL, options); /*设置索引0的信号量*/ 

/*打印出信号量的值*/ 
i = semctl(id, 0, GETVAL, 0); 
printf("value of semaphore at index 0 is %d\n", i); 

/*下面重新设置信号量*/ 
lock_it.sem_num = 0; /*设置哪个信号量*/ 
lock_it.sem_op = -1; /*定义操作*/ 
lock_it.sem_flg = IPC_NOWAIT; /*操作方式*/ 
if (semop(id, &lock_it, 1) == -1) { 
printf("can not lock semaphore.\n"); 
exit(1); 


i = semctl(id, 0, GETVAL, 0); 
printf("value of semaphore at index 0 is %d\n", i); 

/*清除信号量*/ 
semctl(id, 0, IPC_RMID, 0); 

semget()


     可以使用系统调用semget()创建一个新的信号量集,或者存取一个已经存在的信号量集:

系统调用:semget();
原型:intsemget(key_t key,int nsems,int semflg);
返回值:如果成功,则返回信号量集的IPC标识符。如果失败,则返回-1:errno=EACCESS(没有权限)
EEXIST(信号量集已经存在,无法创建)
EIDRM(信号量集已经删除)
ENOENT(信号量集不存在,同时没有使用IPC_CREAT)
ENOMEM(没有足够的内存创建新的信号量集)
ENOSPC(超出限制)
    系统调用semget()的第一个参数是关键字值(一般是由系统调用ftok()返回的)。系统内核将此值和系统中存在的其他的信号量集的关键字值进行比较。打开和存取操作与参数semflg中的内容相关。IPC_CREAT如果信号量集在系统内核中不存在,则创建信号量集。IPC_EXCL当和 IPC_CREAT一同使用时,如果信号量集已经存在,则调用失败。如果单独使用IPC_CREAT,则semget()要么返回新创建的信号量集的标识符,要么返回系统中已经存在的同样的关键字值的信号量的标识符。如果IPC_EXCL和IPC_CREAT一同使用,则要么返回新创建的信号量集的标识符,要么返回-1。IPC_EXCL单独使用没有意义。参数nsems指出了一个新的信号量集中应该创建的信号量的个数。信号量集中最多的信号量的个数是在linux/sem.h中定义的:
#defineSEMMSL32/*<=512maxnumofsemaphoresperid*/
下面是一个打开和创建信号量集的程序:
intopen_semaphore_set(key_t keyval,int numsems)
{
intsid;
if(!numsems)
return(-1);
if((sid=semget(mykey,numsems,IPC_CREAT|0660))==-1)
{
return(-1);
}
return(sid);
}
};
==============================================================
semop()


系统调用:semop();
调用原型:int semop(int semid,struct sembuf*sops,unsign ednsops);
返回值:0,如果成功。-1,如果失败:errno=E2BIG(nsops大于最大的ops数目)
EACCESS(权限不够)
EAGAIN(使用了IPC_NOWAIT,但操作不能继续进行)
EFAULT(sops指向的地址无效)
EIDRM(信号量集已经删除)
EINTR(当睡眠时接收到其他信号)
EINVAL(信号量集不存在,或者semid无效)
ENOMEM(使用了SEM_UNDO,但无足够的内存创建所需的数据结构)
ERANGE(信号量值超出范围)

    第一个参数是关键字值。第二个参数是指向将要操作的数组的指针。第三个参数是数组中的操作的个数。参数sops指向由sembuf组成的数组。此数组是在linux/sem.h中定义的:
/*semop systemcall takes an array of these*/
structsembuf{
ushortsem_num;/*semaphore index in array*/
shortsem_op;/*semaphore operation*/
shortsem_flg;/*operation flags*/
sem_num将要处理的信号量的个数。
sem_op要执行的操作。
sem_flg操作标志。
    如果sem_op是负数,那么信号量将减去它的值。这和信号量控制的资源有关。如果没有使用IPC_NOWAIT,那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。如果sem_op是正数,则信号量加上它的值。这也就是进程释放信号量控制的资源。最后,如果sem_op是0,那么调用进程将调用sleep(),直到信号量的值为0。这在一个进程等待完全空闲的资源时使用。
===============================================================
semctl()


系统调用:semctl();
原型:int semctl(int semid,int semnum,int cmd,union semunarg);
返回值:如果成功,则为一个正数。
如果失败,则为-1:errno=EACCESS(权限不够)
EFAULT(arg指向的地址无效)
EIDRM(信号量集已经删除)
EINVAL(信号量集不存在,或者semid无效)
EPERM(EUID没有cmd的权利)
ERANGE(信号量值超出范围)

    系统调用semctl用来执行在信号量集上的控制操作。这和在消息队列中的系统调用msgctl是十分相似的。但这两个系统调用的参数略有不同。因为信号量一般是作为一个信号量集使用的,而不是一个单独的信号量。所以在信号量集的操作中,不但要知道IPC关键字值,也要知道信号量集中的具体的信号量。这两个系统调用都使用了参数cmd,它用来指出要操作的具体命令。两个系统调用中的最后一个参数也不一样。在系统调用msgctl中,最后一个参数是指向内核中使用的数据结构的指针。我们使用此数据结构来取得有关消息队列的一些信息,以及设置或者改变队列的存取权限和使用者。但在信号量中支持额外的可选的命令,这样就要求有一个更为复杂的数据结构。
系统调用semctl()的第一个参数是关键字值。第二个参数是信号量数目。
    参数cmd中可以使用的命令如下:
    ·IPC_STAT读取一个信号量集的数据结构semid_ds,并将其存储在semun中的buf参数中。
    ·IPC_SET设置信号量集的数据结构semid_ds中的元素ipc_perm,其值取自semun中的buf参数。
    ·IPC_RMID将信号量集从内存中删除。
    ·GETALL用于读取信号量集中的所有信号量的值。
    ·GETNCNT返回正在等待资源的进程数目。
    ·GETPID返回最后一个执行semop操作的进程的PID。
    ·GETVAL返回信号量集中的一个单个的信号量的值。
    ·GETZCNT返回这在等待完全空闲的资源的进程数目。
    ·SETALL设置信号量集中的所有的信号量的值。
    ·SETVAL设置信号量集中的一个单独的信号量的值。
    参数arg代表一个semun的实例。semun是在linux/sem.h中定义的:
/*arg for semctl systemcalls.*/
unionsemun{
intval;/*value for SETVAL*/
structsemid_ds*buf;/*buffer for IPC_STAT&IPC_SET*/
ushort*array;/*array for GETALL&SETALL*/
structseminfo*__buf;/*buffer for IPC_INFO*/
void*__pad;
    val当执行SETVAL命令时使用。buf在IPC_STAT/IPC_SET命令中使用。代表了内核中使用的信号量的数据结构。array在使用GETALL/SETALL命令时使用的指针。
    下面的程序返回信号量的值。当使用GETVAL命令时,调用中的最后一个参数被忽略:
intget_sem_val(intsid,intsemnum)
{
return(semctl(sid,semnum,GETVAL,0));
}
    下面是一个实际应用的例子:
#defineMAX_PRINTERS5
printer_usage()
{
int x;
for(x=0;x<MAX_PRINTERS;x++)
printf("Printer%d:%d\n\r",x,get_sem_val(sid,x));
}
    下面的程序可以用来初始化一个新的信号量值:
void init_semaphore(int sid,int semnum,int initval)
{
union semunsemopts;
semopts.val=initval;
semctl(sid,semnum,SETVAL,semopts);
}
    注意系统调用semctl中的最后一个参数是一个联合类型的副本,而不是一个指向联合类型的指针。

 


   2.3.5 套接口 
    套接口(socket)编程是实现Linux系统和其他大多数操作系统中进程间通信的主要方式之一。我们熟知的WWW服务、FTP服务、TELNET服务 等都是基于套接口编程来实现的。除了在异地的计算机进程间以外,套接口同样适用于本地同一台计算机内部的进程间通信。关于套接口的经典教材同样是 Richard Stevens编著的《Unix网络编程:联网的API和套接字》,清华大学出版社出版了该书的影印版。它同样是Linux程序员的必备书籍之一。 
    关于这一部分的内容,可以参照本文作者的另一篇文章《设计自己的网络蚂蚁》,那里由常用的几个套接口函数的介绍和示例程序。这一部分或许是Linux进程 间通信编程中最须关注和最吸引人的一部分,毕竟,Internet 正在我们身边以不可思议的速度发展着,如果一个程序员在设计编写他下一个程序的时候,根本没有考虑到网络,考虑到Internet,那么,可以说,他的设 计很难成功。 

3 Linux的进程和Win32的进程/线程比较 
   熟悉WIN32编程的人一定知道,WIN32的进程管理方式与Linux上有着很大区别,在UNIX里,只有进程的概念,但在WIN32里却还有一个"线程"的概念,那么Linux和WIN32在这里究竟有着什么区别呢? 
    WIN32里的进程/线程是继承自OS/2的。在WIN32里,"进程"是指一个程序,而"线程"是一个"进程"里的一个执行"线索"。从核心上讲, WIN32的多进程与Linux并无多大的区别,在WIN32里的线程才相当于Linux的进程,是一个实际正在执行的代码。但是,WIN32里同一个进 程里各个线程之间是共享数据段的。这才是与Linux的进程最大的不同。 
   下面这段程序显示了WIN32下一个进程如何启动一个线程。 

int g; 
DWORD WINAPI ChildProcess( LPVOID lpParameter ){ 
int i; 
for ( i = 1; i <1000; i ++) { 
g ++; 
printf( "This is Child Thread: %d\n", g ); 

ExitThread( 0 ); 
}; 

void main() 

int threadID; 
int i; 
g = 0; 
CreateThread( NULL, 0, ChildProcess, NULL, 0, &threadID ); 
for ( i = 1; i <1000; i ++) { 
g ++; 
printf( "This is Parent Thread: %d\n", g ); 



    在WIN32下,使用CreateThread函数创建线程,与Linux下创建进程不同,WIN32线程不是从创建处开始运行的,而是由 CreateThread指定一个函数,线程就从那个函数处开始运行。此程序同前面的UNIX程序一样,由两个线程各打印1000条信息。 threadID是子线程的线程号,另外,全局变量g是子线程与父线程共享的,这就是与Linux最大的不同之处。大家可以看出,WIN32的进程/线程 要比Linux复杂,在Linux要实现类似WIN32的线程并不难,只要fork以后,让子进程调用ThreadProc函数,并且为全局变量开设共享 数据区就行了,但在WIN32下就无法实现类似fork的功能了。所以现在WIN32下的C语言编译器所提供的库函数虽然已经能兼容大多数 Linux/UNIX的库函数,但却仍无法实现fork。 
   对于多任务系统,共享数据区是必要的,但也是一个容易引起混乱的问题,在WIN32下,一个程序员很容易忘记线程之间的数据是共享的这一情况,一个线程修 改过一个变量后,另一个线程却又修改了它,结果引起程序出问题。但在Linux下,由于变量本来并不共享,而由程序员来显式地指定要共享的数据,使程序变 得更清晰与安全。 
至于WIN32的"进程"概念,其含义则是"应用程序",也就是相当于UNIX下的exec了。 
   Linux也有自己的多线程函数pthread,它既不同于Linux的进程,也不同于WIN32下的进程,关于pthread的介绍和如何在Linux环境下编写多线程程序我们将在另一篇文章《Linux下的多线程编程》中讲述。  


2016-02-17 23:19:37 qq_15437667 阅读数 2312

unix系统中,可以用于进程间的通信的机制

首先,个人主要写的是服务器代码,主要使用的是,主机间用socket、或者消息队列,同一主机之间用消息队列,控制通信使用socket,反正主要还是用的这俩居多,不过有的时候还是会使用内存共享,其他的机制,恕我无知,我真的没怎么用过。

socket主要是非阻塞式,消息队列主要用的是开源的zeromq(我使用的C/C++),共享内存和内存映射主要用于同一主机间消息量很多的场景,但需要注意使用锁。

大概介绍

  1. 管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。
  2. 命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。
  3. 信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。
  4. 消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺
  5. 共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
  6. 内存映射(mapped memory):内存映射允许任何多个进程间通信,每一个使用该机制的进程通过把一个共享的文件映射到自己的进程地址空间来实现它。
  7. 信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。
  8. 套接口(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。
2016-12-05 12:39:41 u010853261 阅读数 2541

最近在看Java的NIO的JDK源码时,发现在Selector的实现的底层用到了pipe管道的知识,于是就来学习记录一波。

1.进程间利用管道通信的应用场景

主要是数据传输:一个进程需要将数据发送给另外一个进程;

2. 管道通信:

一个进程在管道的尾部写入数据,另一个进程从管道的头部读出数据。管道包括无名管道有名管道两种,前者只能用于父进程和子进程间的通信,后者可用于运行于同一系统中的任意两个进程间的通信。下面用一个示意图来表示:
管道通信的读与写

3. 管道通信的特点

  1. 管道通讯是单向的,先进先出,有固定的读端和写端。
  2. 数据被进程从管道读出后,在管道中该数据就不存在了。
  3. 当进程去读取空管道的时候,进程会阻塞。
  4. 当进程往满管道写数据时,进程会阻塞。
  5. 管道容量为64KB(#define PIPE_BUFFERS 16
    include/linux/pipe_fs_i.h)

4.无名管道

在Linux系统中,无名管道一旦创建完成后,操作无名管道等同于操作文件。无名管道的读端被视作一个文件;无名管道的写端也被视作一个文件。

发送进程利用文件系统的系统调用write(fd[1],buf,size),把buf种的长度为size字符的消息送入管道入口fd[1],接收进程则使用系统调用read(fd[0],buf,size)从管道出口fd[0]出口读出size字符的消息置入buf中。这里,管道按FIFO(先进先出)方式传送消息,且只能单向传送消息(如图)。
这里写图片描述

利用UNIX提供的系统调用pipe,可建立一条同步通信管道。其格式为:
int fd[2];
pipe(fd)
这里,fd[1]为写入端,fd[0]为读出端。

/*
创建无名管道,用于父子进程间通信,在创建子进程之前创建无名管道
*/
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>

void main()
{
    pid_t pid = 0;
    int pipefd[2];/定义文件描述符,[0]是读端;[1]是写端。
    char c_buf[10];//缓冲区
    /*创建管道*/
    pipe(pipefd);

    /*创建子进程*/
    pid = fork();

    if(pid > 0)
    {
        /*父进程写入数据*/
        write(pipefd[1], "hello",6);
        wait(NULL);
        close(pipefd[1]);
        exit(0);
    }
    if(pid == 0)
    {
        /*子进程读取数据*/
        read(pipefd[0], c_buf, 6); 
        printf( "child read %s\n", c_buf);
        close(pipefd[0]);
        exit(0);
    }   
}

运行结果如图:
这里写图片描述

2013-12-26 16:19:55 mcgrady_tracy 阅读数 850
管道的特性:
1. 半双工通信;
2. 只能在父进程和子进程或兄弟进程之间通信;

Unix/Linux中使用pipe函数创建管道,原型如下:
#include <unistd.h>

int pipe(int fildes[2]);
成功返回0,失败返回-1,参数fildes为返回的两个文件描述符,fildes[0]用于读,fildes[1]用于写。

实例如下:
/* http://beej.us/guide/bgipc/example/pipe1.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <unistd.h>

int main(void)
{
	int pfds[2];
	char buf[30];

	if (pipe(pfds) == -1) {
		perror("pipe");
		exit(1);
	}

	printf("writing to file descriptor #%d\n", pfds[1]);
	write(pfds[1], "test", 5);
	printf("reading from file descriptor #%d\n", pfds[0]);
	read(pfds[0], buf, 5);
	printf("read \"%s\"\n", buf);

	return 0;
}

使用fork的实例:
/* http://beej.us/guide/bgipc/example/pipe2.c */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/types.h>
#include <unistd.h>

int main(void)
{
        int pfds[2];
        char buf[30];

        pipe(pfds);

        if (!fork()) {
                printf(" CHILD: writing to the pipe\n");
                write(pfds[1], "test", 5);
                printf(" CHILD: exiting\n");
                exit(0);
        } else {
                printf("PARENT: reading from pipe\n");
                read(pfds[0], buf, 5);
                printf("PARENT: read \"%s\"\n", buf);
                wait(NULL);
        }

        return 0;
}

另一个例子:
/* http://beej.us/guide/bgipc/example/pipe3.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
        int pfds[2];

        pipe(pfds);

        if (!fork()) {
                close(1);       /* close normal stdout */
                dup(pfds[1]);   /* make stdout same as pfds[1] */
                close(pfds[0]); /* we don't need this */
                execlp("ls", "ls", NULL);
        } else {
                close(0);       /* close normal stdin */
                dup(pfds[0]);   /* make stdin same as pfds[0] */
                close(pfds[1]); /* we don't need this */
                execlp("wc", "wc", "-l", NULL);
        }

        return 0;
}
该程序相当于执行命令ls | wc -l。
2016-08-11 21:21:01 caoyan_12727 阅读数 2778

    在之前的博客中已经总结了其它7种进程间的通信方式。unix域套接字用于在同一台计算机上的进程间通信,虽然因特网域套接字可用于同一目的,但是unix域套接字的效率更高。unix域套接字并不进行协议处理,不需要添加或删除网络报头,无需计算校验和,不需要产生顺序号,无需发送确认报文。UNIX与套接字提供和数据报两种接口,UNIX域数据报服务是可靠的,就不会丢失消息也不会传递出错。UNIX域套接字是套接字和管道之间的混合物。为了创建一对非命名的,相互连接的UNXI域套接字,用户可以使用socketopair函数。创建一对互相连接的unix域套接字可以用socketpair函数:

1.unix未命名套接字

#include<sys/socket.h>  
int socketpari(int domain, int type, int protocol, int sockfd[2]); //若成功则返回0,出错则返回-1.  
《在unix环境高级编程》一书中是这样描述的:



下面我们就用一个简单的程序来验证上面的管道模型是否正确!!!

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <iostream>
#include <unistd.h> 
using namespace std;

int main(void){
        int fd[2];
        int pid;
	int n=0;
        char wbuf[16] = "1234567890";
        char rbuf[16]={'\0'};
        if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
                printf("socketpair error!\n");
                return -1;
        }

        if((pid = fork())<0){
                printf("fork error!\n");
                return -1;
        }else if(pid == 0){//child
                close(fd[0]);
                if((n=write(fd[1],wbuf,strlen(wbuf))) < 0){
                        printf("child write error!\n");
                        exit(-1);
                }
                sleep(1);//子进程等待1秒
		cout<<"the data of rbuf of child before commubication is :"<<rbuf<<endl;
                if((n=read(fd[1],rbuf,16))<0){//子进程从fd[1]读取数据
                	printf("child read error!\n");
                        exit(-1);
                }
		rbuf[n]=0;
                printf("son read data from father: %s\n",rbuf);
                exit(0);
        }else{//parent
        	sleep(1);//父进程等待1秒钟
                close(fd[1]);
		cout<<"the data of rbuf of parent before commubication is :"<<rbuf<<endl;
                if((n=read(fd[0],rbuf,16)) < 0){
                        printf("parent read error!\n");
                        exit(-1);
                }
		rbuf[n]=0;
                printf("parent read data from son :%s\n",rbuf);
                if(write(fd[0],wbuf,strlen(wbuf)) < 0){
                        printf("parent write error!\n");
                        exit(-1);
                }
                exit(0);
        }
        return 0;
}
测试结果:


   由图可以看出父进程和子进程构成的通道正如图17-1所示,也就是说父进程或子进程都可以向它们的唯一端口fd[0]或fd[1]同时读写数据,而这一切都是因为unix域套接字的声明。接下来我们来验证声明一个管道,看能不能在同一段进行读写:

#include <stdio.h>
#include <stdlib.h>
#include <sys/socket.h>
#include <string.h>
#include <iostream>
#include <unistd.h> 
using namespace std;

int main(void){
        int fd[2];
        int pid;
	int n=0;
        char wbuf[16] = "1234567890";
        char rbuf[16]={'\0'};
	if(pipe(fd) < 0){
        //if(socketpair(AF_UNIX, SOCK_STREAM, 0, fd) < 0){
                printf("socketpair error!\n");
                return -1;
        }

        if((pid = fork())<0){
                printf("fork error!\n");
                return -1;
        }else if(pid == 0){//child
                close(fd[0]);
                if((n=write(fd[1],wbuf,strlen(wbuf))) < 0){
                        printf("child write error!\n");
                        exit(-1);
                }
                sleep(1);//子进程等待1秒
		cout<<rbuf<<endl;
                if((n=read(fd[1],rbuf,16))<0){//子进程从fd[1]读取数据
                	printf("child read error!\n");
                        exit(-1);
                }
		rbuf[n]=0;
                printf("child read data from father: %s\n",rbuf);
                exit(0);
        }else{//parent
        	sleep(1);//父进程等待1秒钟
                close(fd[1]);
		cout<<rbuf<<endl;
                if((n=read(fd[0],rbuf,16)) < 0){
                        printf("parent read error!\n");
                        exit(-1);
                }
		rbuf[n]=0;
                printf("parent read data from son :%s\n",rbuf);
                if(write(fd[0],wbuf,strlen(wbuf)) < 0){
                        printf("parent write error!\n");
                        exit(-1);
                }
                exit(0);
        }
        return 0;
}
测试结果:


从测试结果中可以看出,对于管道来说,我们不能在管道的同一端进行读写,这样只能报错!!!!!

unix域套接字的经典应用:

我们知道消息队列存在一个问题,即不能将它们和poll或者select一起使用,这是因为它们不能关联到文件描述符,然而,套接字和文件描述符是相关联的消息到达时,可以用套接字来通知,即消息到达时我们将起写入到套接字的一端,那么我们可以用poll来轮询unix域套接字的另一端即可判断消息有没有到来,这个时候相当于多了一层中介。下面就来看看这个非常巧妙的设计:

server.c

<span style="color:#000000;">#include "apue.h"
#include <poll.h>
#include <pthread.h>
#include <sys/msg.h>
#include <sys/socket.h>
#include<iostream>
using namespace std;

#define NQ  3 /* number of queues */
#define MAXMSZ	512		/* maximum lenth */
#define KEY	0x123	/* key for first message queue */

struct threadinfo {
	int qid;// the quene id 
	int fd;// the fd used to write
};

struct mymesg {
	long mtype;
	char mtext[MAXMSZ];
};

void * helper(void *arg){
	int n;
	struct mymesg m;
	struct threadinfo *tip = (struct threadinfo *)arg;
	for(;;) {
		memset(&m, 0, sizeof(m));
		if ((n = msgrcv(tip->qid, &m, MAXMSZ, 0, MSG_NOERROR)) < 0)printf("msgrcv error\n");
		if (write(tip->fd, m.mtext, n) < 0)printf("write error\n");
	}
}

int main(){
	int i,n,err;
	int fd[2];
	int qid[NQ];
	struct pollfd pfd[NQ];
	struct threadinfo ti[NQ];
	pthread_t tid[NQ];
	char buf[MAXMSZ];
	for (i = 0; i < NQ; i++) {
		if ((qid[i] = msgget((KEY+i), IPC_CREAT|0666)) < 0)printf("msgget error\n");
		cout<<"the msgque id of server is "<<qid[i]<<endl;
		printf("queue ID %d is %d\n", i, qid[i]);
		if (socketpair(AF_UNIX, SOCK_DGRAM, 0, fd) < 0)printf("socketpair error\n");
		cout<<"fd[0] is: "<<fd[0]<<"fd[1] is: "<<fd[1]<<endl;
		pfd[i].fd = fd[0];
		pfd[i].events = POLLIN;
		ti[i].qid = qid[i];
		ti[i].fd = fd[1];
		if ((err = pthread_create(&tid[i], NULL, helper, &ti[i])) != 0)printf("pthread_create error\n");
	}
	for (;;) {//reading data
		if (poll(pfd, NQ, -1) < 0)printf("poll error\n");
		for (i = 0; i < NQ; i++) {
			if (pfd[i].revents & POLLIN) {
				if ((n = read(pfd[i].fd, buf, sizeof(buf))) < 0)printf("read error\n");
				buf[n] = 0;
				printf("queue id %d, message %s\n", qid[i], buf);
			}
		}
	}
	exit(0);
}

</span>
client.c

<span style="color:#000000;">#include "apue.h"
#include <sys/msg.h>
#include<iostream>
using namespace std;

#define MAXMSZ 512

struct mymesg {
	long mtype;
	char mtext[MAXMSZ];
};

int main(int argc, char *argv[]){
	key_t key;
	long qid;
	size_t nbytes;
	struct mymesg m;
	if (argc != 3) {
		fprintf(stderr, "usage: sendmsg KEY message\n");
		exit(1);
	}
	key = strtol(argv[1], NULL, 0);
	if ((qid = msgget(key, 0)) < 0)printf("can't open queue key %s", argv[1]);
	cout<<"the id of the queue is"<<qid<<endl;
	memset(&m, 0, sizeof(m));
	strncpy(m.mtext, argv[2], MAXMSZ-1);
	nbytes = strlen(m.mtext);
	m.mtype = 1;
	if (msgsnd(qid, &m, nbytes, 0) < 0)printf("can't send message\n");
	exit(0);
}</span>
测试结果:


从图中我们可以看到,1.服务器段利用poll来间接轮询来自客户端的消息;2.对于msgget,如果使用相同的key(比如ox123)那么得到的消息队列的id是一样的!!!!

2.unix命名域套接字

   虽然socketpair函数能创建一对相互连接的套接字,但是每一个套接字却没有名字,无关的进程不能使用它们。如果我们绑定一个地址,这个地址就是一个路径,那么无关的进程就可以使用它们,下面是地址的格式:

<span style="color:#000000;">struct sockaddr_un {
 sa_family_t sun_family; /*PF_UNIX或AF_UNIX */
 char sun_path[UNIX_PATH_MAX]; /* 路径名 */
};</span>
下面我们来看以个简单的例子:

#include "apue.h"
#include <sys/socket.h>
#include <sys/un.h>

int main(){
	int fd, size;
	struct sockaddr_un un;
	un.sun_family = AF_UNIX;
	strcpy(un.sun_path, "/home/caoyan/unix/unix-domain-socket/socket");
	if ((fd = socket(AF_UNIX, SOCK_STREAM, 0)) < 0)printf("socket failed\n");
	size = offsetof(struct sockaddr_un, sun_path) + strlen(un.sun_path);//the total lenght of address
	if (bind(fd, (struct sockaddr *)&un, size) < 0)
		printf("bind failed\n");
	printf("UNIX domain socket bound\n");
	exit(0);
}
其中offsetof是个宏:

#define offsetof(TYPE,MEMBER)  ((int)&(TYPE *)0->MEMBER)

测试结果:

从下面的结果我们可以看出,我们的文件夹里多了一个管道文件socket,这个文件的大小为0。


第16章 进程间通信

阅读数 423

没有更多推荐了,返回首页