unix一对多通信 - CSDN
  • unix进程通信方式总结(上)

    千次阅读 2016-08-06 00:52:21
    本文将《unix环境高级编程》书中所涉及的几种重要的进程间通信方式(Inter-Process Communication)进行简单总结,总的来说,进程间通信有以下几种: (1).
      本文将《unix环境高级编程》一书中所涉及的几种重要的进程间通信方式(Inter-Process Communication)进行简单总结,总的来说,进程间通信有以下几种:
            (1)管道(pipe,未命名管道):适用于两个相关进程间的使用,而且这两个相关的进程还要有一个共同的创建了它们的祖先进程。首先我们先列管道的相关函数。创建一个管道:int pipe(int fd[2]);在历史上,管道是半双工的,数据只能在一个方向上流动。通常,一个管道由一个进程创建,在进程fork之后,这个管道就能在父进程和子进程间使用了。一般的描绘半双工管道的方法如下:



    一般说来,fd[0]为读而打开,fd[1]为写而打开,fd[1]的输出是fd[0]的写入.一旦创建了一个管道,我们就可以像读写文件描述符一样
    让我们来看一个简单的代码:
    #include "apue.h"
    int main(void){
    	int		n;
    	int		fd[2];
    	pid_t	pid;
    	char	line[MAXLINE]
    
    	if (pipe(fd) < 0)
    		err_sys("pipe error");
    	if ((pid = fork()) < 0) {
    		err_sys("fork error");
    	} else if (pid > 0) {		/* parent */
    		close(fd[0]);//父进程关闭读入端,则说明父进程使用写入端
    		write(fd[1], "hello world\n", 12);//父进程将信息写入管道
    	} else {					/* child */
    		close(fd[1]);//子进程关闭写入端,使用读端
    		n = read(fd[0], line, MAXLINE);//从fd[0]将数据读入到缓冲line中
    		write(STDOUT_FILENO, line, n);//将line中的字符写入到标准输出
    	}
    	exit(0);
    }
    
    那么上述代码所形成的管道数据传输示意图就像图15-4所示;
     (2)命名管道(FIFO):
    命名管道相关的函数为:

    通过FIFO,不相关的进程也能进行通信,FIFO有以下两种用途:
    1.shell命令使用FIFO将数据从一条管道复制到另一条管道时无需创建中间临时文件;考虑这样的一个过程,它需要对一个经过过滤的输入流进行两次处理。下图现显示了这种安排。

    数据经prog1处理之后要作为prog3和prog2的输入,如果使用管道(pipe)的话,主程序要fork两次,况且一条管道只能维持一对进程的通信,因为管道是半双工,数据只能从一个方向流向另一个方向,主程序prog1不可能通过一个文件描述符向两个子进程传送数据(一个fd[1]只能向一个fd[0]传送数据),所以此时只能将prog1的输出保存到文件,然后再从文件到prog2,这样的话势必会产生磁盘中间文件。但是如果使用FIFO情况就会不一样,我们来看看书中的例子:
    mkfifo  fifo1
    prog3<fifo1 &
    prog<infile|tee fifo1 |prog2
    用FIFO就可实现这样的过程而中间文件的产生,如图:

    为了了解这个过程我们来看看上面的三条语句使怎样工作的:
       tee命令:在执行Linux命令时,我们可以把输出重定向到文件中,比如 ls >a.txt,这时我们就不能看到输出了,如果我们既想把输出保存到文件中,又想在屏幕上看到输出内容,就可以使用tee命令了。tee命令读取标准输入,把这些内容同时输出到标准输出和(多个)文件中(read from standard input and write to standard output and files. Copy standard input to each FILE, and also to standard output. If a FILE is -, copy again to standard output.)。在info tee中说道:tee命令可以重定向标准输出到多个文件(`tee': Redirect output to multiple files. The `tee' command copies standard input to standard output and also to any files given as arguments.  This is useful when you want not only to send some data down a pipe, but also to save a copy.)。要注意的是:在使用管道线时,前一个命令的标准错误输出不会被tee读取。简言之tee的作用是:输出到标准输出的同时,保存到文件file中。如果文件不存在,则创建;如果已经存在,则覆盖之。
       新建一个命名管道:FIFO,然后后台运行prog3<fifo1 &,这个表示一旦有数据写入到管道fifo1时,prog3就从管道读取数据;prog<infile|tee fifo1 |prog2,这条命令是从infile读入进程prog1所需的输入数据,然后通过prog1对输入数据进行处理,处理之后的输出数据经过|变成tee命令的输入,tee将起输入不仅显示到标准输出,而且还被输出到fifo1文件(unix一切皆可看做是文件);那么fifo1里的数据会被读入到进程prog1,被显示到标准输出的那一份数据又被|命令处理为标准输入被穿到porg2.所以看到整个过程没有中间磁盘文件的产生,虽然要产生fifo1,但是管道文件在磁盘中显示的占用空间大小为0(即不占用磁盘空间)。
    2.客户进程-服务器进程应用程序中,FIFO用作汇聚点,在客户进程和服务器进程二者之间进行数据传递。
    首先用我自己写的小程序来验证FIFO命名管道的基本用法:
    /*fifo-write.c*/
    #include <stdio.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    #include<apue.h>
    #include <unistd.h>
    
    int main(){
    	int fd;
    	int nRead;
    	char szBuff[100];
    	sleep(1);//wait for the creating of fifo3
    	fd=open("/home/caoyan/unix/c15/cyfifo/fifo3", O_WRONLY);
      	while(1){
          		if((nRead = read(STDIN_FILENO, szBuff, sizeof(szBuff))) == -1){
              	if (errno == EAGAIN)
                		printf("no data\n");
            	}
          		szBuff[nRead] = '\0';
         	 	write(fd,szBuff,nRead);
    		if (szBuff[0] == '#')break;//the last letter is 'Q' means that the data transport is over!
        	}
    	printf("data sending has finished!\n");
    }
    /*fifo-read.c*/
    #include <stdio.h>
    #include <sys/stat.h>
    #include <errno.h>
    #include <fcntl.h>
    #include<apue.h>
    #include<iostream>
    using namespace std;
    int main(int argc, char* argv[]){
      int tmp,nRead,fd;
      char szBuff[128];
      const char *szPath="/home/caoyan/unix/c15/cyfifo/fifo3";         
      tmp=mkfifo(szPath,0777);
      if (-1 == fd){
          printf("create fifo error\n");
          return 0;
        }
      fd=open("/home/caoyan/unix/c15/cyfifo/fifo3", O_RDONLY);
      if(fd==-1)exit(1);
      while(1){
          if((nRead = read(fd, szBuff, sizeof(szBuff))) == -1){
              if (errno == EAGAIN)
                printf("no data\n");
            }
          if (szBuff[0] == '#')break;//the last letter is 'Q' means that the data transport is over!
          szBuff[nRead] = '\0';
          printf("%s", szBuff);
        }
       printf("data recieving has finished!\n");
      return 0;
    }
    测试结果:
    数据发送方:

    数据接收方:

    可以看到两个无关的进程通信成功!!!
    让我们来看看通信结束之后的命名管道fifo3:

    可以看到管道使用前后大小均为0!!!!
    下面我们来看看用FIFO实现客户进程和服务器进程通信的设计方式:

    图15-22表示多个客户进程向服务器进程请求数据,每个客户进程可以将其请求写入到一个总所周知的FIFO,如果所有客户进程与服务器进程共用一个FIFO的话(这里的FIFO是另外的一个命名管道,不同于上图中的众所周知的FIFO,用来从服务器进程写入,从客户进程读出数据),服务器进程就不知道如何将数据回送给客户进程,因为一旦有数据被写入到FIFO中,所有的客户进程都可以读里面的数据,这样就会出乱!!!一个好的解决方案如下:
    为每个客户进程建立一个FIFO,这样每个客户进程就可以互不干扰地从服务器进程读取数据!!!!

    (3)消息队列:
     第一步:创建一个消息队列:int msgget(key_t key,int msgflg),其中key_t ftok( const char * fname, int id ),fname就时你指定的文件名(该文件必须是存在而且可以访问的),id是子序号,虽然为int,但是只有8个比特被使用(0-255)。当成功执行的时候,一个key_t值将会被返回,否则 -1 被返回。在一般的UNIX实现中,是将文件的索引节点号取出,前面加上子序号得到key_t的返回值。如指定文件的索引节点号为65538,换算成16进制为 0x010002,而你指定的ID值为38,换算成16进制为0x26,则最后的key_t返回值为0x26010002。也是说消息队列号由文件的索引节点号和用户指定ID组成;
    第二步:发送消息 :int msgsnd ( int msqid, struct msgbuf *msgp, int msgsz, int msgflg ); 其中的msgbuf的数据结构可表示为:
    (1)消息缓冲区(msgbuf)
    我们在这里要介绍的第一个数据结构是msgbuf结构,可以把这个特殊的数据结构看成一个存放消息数据的模板,它在include/linux/msg.h中声明,描述如下:
    /* msgsnd 和msgrcv 系统调用使用的消息缓冲区*/
    struct msgbuf {
        long mtype;         /* 消息的类型,必须为正数 */
        char mtext[1];      /* 消息正文 */
    };
    注意:消息正文的长度是可以改变的,它的长度可以是1字节也可以是512字节或者更长,这也就是为什么在发送消息的时候要指明消息正文的长度,一般而言,消息正文的前面的消息类型大小是固定的。对于消息数据元素(mtext),不要受其描述的限制。实际上,这个域(mtext)不仅能保存字符数组,而且能保存任何形式的任何数据。这个域本身是任意的,因为这个结构本身可以由应用程序员重新定义:
    struct my_msgbuf {
          long    mtype;          /* 消息类型 */
            long    request_id;     /* 请求识别号 */
            struct client info;    /* 客户消息结构 */
    };
    我们看到,消息的类型还是和前面一样,但是结构的剩余部分由两个其它的元素代替,而且有一个是结构。这就是消息队列的优美之处,内核根本不管传送的是什么样的数据,任何信息都可以传送。
    但是,消息的长度还是有限制的,在Linux中,给定消息的最大长度在include/linux/msg.h中定义如下:
    #define MSGMAX 8192    /* max size of message (bytes) */
    消息总的长度不能超过8192字节,包括mtype域,它是4字节长。
    (2)消息结构(msg)
    内核把每一条消息存储在以msg结构为框架的队列中,它在include/ linux/msg.h中定义如下:
    struct msg {
        struct msg *msg_next;   /* 队列上的下一条消息 */
        long msg_type;          /*消息类型*/
        char *msg_spot;         /* 消息正文的地址 */
        short msg_ts;           /* 消息正文的大小 */
    };
    注意:msg_next是指向下一条消息的指针,它们在内核地址空间形成一个单链表。
    (3)消息队列结构(msgid_ds)
    当在系统中创建每一个消息队列时,内核创建、存储及维护这个结构的一个实例。
    /* 在系统中的每一个消息队列对应一个msqid_ds 结构 */
    struct msqid_ds {
        struct ipc_perm msg_perm;
        struct msg *msg_first;    /* 队列上第一条消息,即链表头*/
        struct msg *msg_last;    /* 队列中的最后一条消息,即链表尾 */
        time_t msg_stime;        /* 发送给队列的最后一条消息的时间 */
        time_t msg_rtime;            /* 从消息队列接收到的最后一条消息的时间 */
        time_t msg_ctime;             /* 最后修改队列的时间*/
        ushort msg_cbytes;          /*队列上所有消息总的字节数 */
        ushort msg_qnum;          /*在当前队列上消息的个数 */
        ushort msg_qbytes;        /* 队列最大的字节数 */
        ushort msg_lspid;           /* 发送最后一条消息的进程的pid */
        ushort msg_lrpid;           /* 接收最后一条消息的进程的pid */
    };
    那么消息队列的逻辑结构可以表示为:

    第四步:接受消息:int msgrcv ( int msqid, struct msgbuf *msgp, int msgsz, long mtype, int msgflg );
    返回值:成功,则为拷贝到消息缓冲区的字节数,失败为-1。
    很明显,第一个参数用来指定要检索的队列(必须由msgget()调用返回),第二个参数(msgp)是存放检索到消息的缓冲区的地址,第三个参数(msgsz)是消息缓冲区的大小,不包括消息类型mtype的长度。第四个参数(mtype)指定了消息的类型。内核将搜索队列中相匹配类型的最早的消息,并且返回这个消息的一个拷贝,返回的消息放在由msgp参数指向的地址。这里存在一个特殊的情况,如果传递给mytype参数的值为0,就可以不管类型,只返回队列中最早的消息。如果传递给参数msgflg的值为IPC_NOWAIT,并且没有可取的消息,那么给调用进程返回ENOMSG错误消息,否则,调用进程阻塞,直到一条消息到达队列并且满足msgrcv()的参数。如果一个客户正在等待消息,而队列被删除,则返回EIDRM。如果当进程正在阻塞,并且等待一条消息到达但捕获到了一个信号,则返回EINTR。
    (4)信号量:
    当我们在多用户系统,多进程系统,或是两者混合的系统中使用线程操作编写程序时,我们经常会发现我们有段临界代码,在此处我们需要保证一个进程(或是一个线程的执行)需要排他的访问一个资源。信号量有一个复杂的编程接口。幸运的是,我们可以很容易的为自己提供一个对于大多数的信号量编程问题足够高效的简化接口。为了阻止多个程序同时访问一个共享资源所引起的问题,我们需要一种方法生成并且使用一个标记从而保证在临界区部分一次只有一个线程执行。线程相关的方法,我们可以使用互斥或信号量来控制一个多线程程序对于临界区的访问。信号量与已经介绍过的IPC机构(管道,FIFO以及消息队列不同),它是一个计数器,用于多个为多个进程提供对共享数据的访问。当我们要使用XSI信号量时,首先需要通过调用函数semget来获得一个信号量的ID,函数原型如下
    int semget(key_t key,int nsems,int flag);
    其中,nsems是该集合中的信号量数,如果是创建新集合(一般是在服务器进程中),则必须指定nsems,如果是应用现有集合(一个客户进程),则将nsems指定为0。
    信号量相关的三个重要函数:

    1.   semget函数原型

    semget(得到一个信号量集标识符或创建一个信号量集对象)
    所需头文件 #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    函数说明 得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
    函数原型 int semget(key_t key, int nsems, int semflg)
    函数传入值 key 0(IPC_PRIVATE):会建立新信号量集对象
    大于0的32位整数:视参数semflg来确定操作,通常要求此值来源于ftok返回的IPC键值
    nsems 创建信号量集中信号量的个数,该参数只在创建信号量集时有效
    msgflg 0:取信号量集标识符,若不存在则函数会报错
    IPC_CREAT:当semflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的信号量集,则新建一个信号量集;如果存在这样的信号量集,返回此信号量集的标识符
    IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的信号量集,则新建一个消息队列;如果存在这样的信号量集则报错
    函数返回值 成功:返回信号量集的标识符
    出错:-1,错误原因存于error中
    附加说明 上述semflg参数为模式标志参数,使用时需要与IPC对象存取权限(如0600)进行|运算来确定信号量集的存取权限
    错误代码 EACCESS:没有权限
    EEXIST:信号量集已经存在,无法创建
    EIDRM:信号量集已经删除
    ENOENT:信号量集不存在,同时semflg没有设置IPC_CREAT标志
    ENOMEM:没有足够的内存创建新的信号量集
    ENOSPC:超出限制

    2.   semop函数原型

    semop(完成对信号量的P操作或V操作)
    所需头文件 #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    函数说明 对信号量集标识符为semid中的一个或多个信号量进行P操作或V操作
    函数原型 int semop(int semid, struct sembuf *sops, unsigned nsops)
    函数传入值 semid:信号量集标识符
    sops:指向进行操作的信号量集结构体数组的首地址,此结构的具体说明如下:
    struct sembuf {
        short semnum; /*信号量集合中的信号量编号,0代表第1个信号量*/
        short val;/*若val>0进行V操作信号量值加val,表示进程释放控制的资源 */
    /*若val<0进行P操作信号量值减val,若(semval-val)<0(semval为该信号量值),则调用进程阻塞,直到资源可用;若设置IPC_NOWAIT不会睡眠,进程直接返回EAGAIN错误*/
      /*若val==0时阻塞等待信号量为0,调用进程进入睡眠状态,直到信号值为0;若设置IPC_NOWAIT,进程不会睡眠,直接返回EAGAIN错误*/
        short flag;  /*0 设置信号量的默认操作*/
    /*IPC_NOWAIT设置信号量操作不等待*/
    /*SEM_UNDO 选项会让内核记录一个与调用进程相关的UNDO记录,如果该进程崩溃,则根据这个进程的UNDO记录自动恢复相应信号量的计数值*/
      };
    nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作
    函数返回值 成功:返回信号量集的标识符
    出错:-1,错误原因存于error中
    错误代码 E2BIG:一次对信号量个数的操作超过了系统限制
    EACCESS:权限不够
    EAGAIN:使用了IPC_NOWAIT,但操作不能继续进行
    EFAULT:sops指向的地址无效
    EIDRM:信号量集已经删除
    EINTR:当睡眠时接收到其他信号
    EINVAL:信号量集不存在,或者semid无效
    ENOMEM:使用了SEM_UNDO,但无足够的内存创建所需的数据结构
    ERANGE:信号量值超出范围

    3.   semctl函数原型

    semctl (得到一个信号量集标识符或创建一个信号量集对象)
    所需头文件 #include <sys/types.h>
    #include <sys/ipc.h>
    #include <sys/sem.h>
    函数说明 得到一个信号量集标识符或创建一个信号量集对象并返回信号量集标识符
    函数原型 int semctl(int semid, int semnum, int cmd, union semun arg)
    函数传入值 semid 信号量集标识符
    semnum 信号量集数组上的下标,表示某一个信号量
    cmd 见下文表15-4
    arg union semun {
       short val;          /*SETVAL用的值*/
       struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
       unsigned short* array; /*SETALL、GETALL用的数组值*/
       struct seminfo *buf;   /*为控制IPC_INFO提供的缓存*/
      } arg;
    函数返回值 成功:大于或等于0,具体说明请参照表15-4
    出错:-1,错误原因存于error中
    附加说明 semid_ds结构见上文信号量集内核结构定义
    错误代码 EACCESS:权限不够
    EFAULT:arg指向的地址无效
    EIDRM:信号量集已经删除
    EINVAL:信号量集不存在,或者semid无效
    EPERM:进程有效用户没有cmd的权限
    ERANGE:信号量值超出范围 

    表15-4 semctl函数cmd形参说明表

    命令 解   释
    IPC_STAT 从信号量集上检索semid_ds结构,并存到semun联合体参数的成员buf的地址中
    IPC_SET 设置一个信号量集合的semid_ds结构中ipc_perm域的值,并从semun的buf中取出值
    IPC_RMID 从内核中删除信号量集合
    GETALL 从信号量集合中获得所有信号量的值,并把其整数值存到semun联合体成员的一个指针数组中
    GETNCNT 返回当前等待资源的进程个数
    GETPID 返回最后一个执行系统调用semop()进程的PID
    GETVAL 返回信号量集合内单个信号量的值
    GETZCNT 返回当前等待100%资源利用的进程个数
    SETALL 与GETALL正好相反
    SETVAL 用联合体中val成员的值设置信号量集合中单个信号量的值
    相关的数据结构为:
    对于系统中的每个信号量集,内核维护一个如下的信息结构:
    struct semid_ds {
        struct ipc_permsem_perm ;
        structsem*    sem_base ; //信号数组指针
        ushort        sem_nsem ; //此集中信号个数
        time_t        sem_otime ; //最后一次semop时间
        time_t        sem_ctime ; //最后一次创建时间
    } ;
    某个给定信号量的结构体
    struct sem {
        ushort_t  semval ;  //信号量的值
        short     sempid ;  //最后一个调用semop的进程ID
        ushort    semncnt ; //等待该信号量值大于当前值的进程数(一有进程释放资源 就被唤醒)
        ushort    semzcnt ; //等待该信号量值等于0的进程数
    } ; 
    struct sembuf {
        unsigned short sem_num ; //信号量在信号量集中的index(对哪个信号量操作),如果只有一个信号量,则对应的值为0
        short          sem_op ;  //操作的类型(P操作 还是 V操作)
        short          sem_flg ; //是否等待(当信号量的值不够消耗时 是否等待其他进进程释放资源)
    } ;
    union semun {
       short val;          /*SETVAL用的值*/
       struct semid_ds* buf; /*IPC_STAT、IPC_SET用的semid_ds结构*/
       unsigned short* array; /*SETALL、GETALL用的数组值*/
       struct seminfo *buf;   /*为控制IPC_INFO提供的缓存*/
      }arg;
    对于sembuf结构体中的sem_op值:
    ⑴若sem_op为正,这对应于进程释放占用的资源数。sem_op值加到信号量的值上。(V操作)
    ⑵若sem_op为负,这表示要获取该信号量控制的资源数。信号量值减去sem_op的绝对值。(P操作)
    ⑶若sem_op为0,这表示调用进程希望等待到该信号量值变成0
    如果此时执行的是p操作且信号量值小于sem_op的绝对值(资源不能满足要求),则:
    ⑴若指定了IPC_NOWAIT,则semop()出错返回EAGAIN。
    ⑵若未指定IPC_NOWAIT,则信号量的semncnt值加1(因为调用进程将进入休眠状态),然后调用进程被挂起直至:①此信号量变成大于或等于sem_op的绝对值;②从系统中删除了此信号量,返回EIDRM;③进程捕捉到一个信号,并从信号处理程序返回,返回EINTR。(与消息队列的阻塞处理方式 很相似)
    下面我们通过引用http://blog.csdn.net/liang890319/article/details/8280860所提及的例子来简单讲解上面的参数的运用:
    /*sem_com.h*/
    #ifndef     SEM_COM_H  
    #define     SEM_COM_H  
      
    #include <sys/ipc.h>  
    #include <sys/sem.h>  
      
    union semun  {  
        int val;  
        struct semid_ds *buf;  
        unsigned short *array;
        struct seminfo *buf;  
    };  
      
    int init_sem(int, int);  
    int del_sem(int);  
    int sem_p(int);  
    int sem_v(int);   
      
    #endif /* SEM_COM_H */
    
    /* sem_com.c */  
      
    #include "sem_com.h"  
    int init_sem(int sem_id, int init_value)  {  
        union semun sem_union;  //可以知道对信号量ID的操作都需要用到semun联合体
        //我们想对信号量ID采取什么样的操作就将对应的值设置,然后再设置标志(SETVAL)
        //不同的标志(也就是cmd参数)对应着设置semum联合体里的不同字段的值
        sem_union.val = init_value;  
        if (semctl(sem_id, 0, SETVAL, sem_union) == -1){//设置单个信号量的值
            perror("Initialize semaphore");       
            return -1;  
        }  
        return 0;  
    }  
      
    int del_sem(int sem_id){//从内核中删除该信号量  
        union semun sem_union;  
        if (semctl(sem_id, 0, IPC_RMID, sem_union) == -1){//在semctl中,参数包括对直接操作的信号,但是在semop中,
            perror("Delete semaphore"); //被操作的信号量的下标由sembuf中的相关字段标明 
            return -1;   
        }  
    } 
    
    int sem_p(int sem_id)  { //对信号量的操作,操作的信息以sembuf结构体进行传递 
        struct sembuf sem_b;  
        sem_b.sem_num = 0; /*id,因为集合中只有一个信号量,所以下标为0*/  
        sem_b.sem_op = -1; /* P operation,对信号量减1*/  
        sem_b.sem_flg = SEM_UNDO; //这会使得操作系统跟踪当前进程对信号量所做的改变,而且如果进程终止而没
        //有释放这个信号量, 如果信号量为这个进程所占有,这个标记可以使得操作系统自动释放这个信号量。 
        if (semop(sem_id, &sem_b, 1) == -1){//对信号量进行减1操作
            perror("P operation");  
            return -1;  
        }  
        return 0;  
    }   
     
    int sem_v(int sem_id) { //和sem_p操作相对,对信号量所代表的资源进行释放 
        struct sembuf sem_b;    
        sem_b.sem_num = 0; /* id */  
        sem_b.sem_op = 1; /* V operation */   
        sem_b.sem_flg = SEM_UNDO;   
        if (semop(sem_id, &sem_b, 1) == -1)  {  
            perror("V operation");  
            return -1;  
        }  
        return 0;  
    }  
    
    /* fork.c */   
    #include <sys/types.h>  
    #include <unistd.h>  
    #include <stdio.h>  
    #include <stdlib.h>  
    #include <sys/types.h>  
    #include <sys/ipc.h>  
    #include <sys/shm.h>  
    #define DELAY_TIME      3  
      
    int main(void)  {  
        pid_t result;  
        int sem_id;  
        sem_id = semget(ftok(".", 'a'),  1, 0666|IPC_CREAT); /* 创建一个信号量集,这个信号量集中只有一个信号量*/  
        init_sem(sem_id, 0);  //初始值设为0资源被占用   
        result = fork();  /*调用fork函数,其返回值为result*/
        /*通过result的值来判断fork函数的返回情况,首先进行出错处理*/  
        if(result ==  -1)perror("Fork\n");  
        else if (result == 0) {/*返回值为0代表子进程*/  
            printf("Child process will wait for some seconds...\n");  
            sleep(DELAY_TIME);  
            printf("The returned value is %d in the child process(PID = %d)\n", result, getpid());  
            sem_v(sem_id);   //释放资源  
        }  
        else {/*返回值大于0代表父进程*/  
            sem_p(sem_id);     //等待资源,如果子进程不释放 就一直等  
            printf("The returned value is %d in the father process(PID = %d)\n", result, getpid());  
            sem_v(sem_id);     //释放资源  
            del_sem(sem_id);  //删除信号量  
        }  
        exit(0);  
    }  
    资源的量一开始被设置为0,即代表当前没用空闲的可用资源,在fork()之后不知道是父进程还是子进程先执行,如果是子进程先执行,那么直接释放资源,父进程直接获得资源无需等待;但如果是父进程先执行,就对资源进行p(申请资源)操作,此时假设未指定IPC_NOWAIT字段,则等待该信号量值大于当前值(0)的进程数semncnt的值会加1,然后父进程被阻塞(一有进程释放资源 就被唤醒)。等到子进程释放之后父进程就可以继续执行。在这个例子中由于子进程sleep(),所以子进程阻塞,等到子进程被唤醒,资源被释放,父进程得以继续执行。
        
            
    展开全文
  • unix进程间通信方式(下)-unix域套接字

    千次阅读 2016-08-11 21:22:01
    unix域套接字用于在同一台计算机上的进程间通信,虽然因特网域套接字可用于同一目的,但是unix域套接字的效率更高。unix
    
    

        在之前的博客中已经总结了其它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。


    展开全文
  • Unix中的IPC(InterProcess Communication)是各种进程通信的统称,在Unix中有很线程间通信方法,但是他们并不是兼容Unix的各种实现,下图列出了Linux系统不同实现所支持的不同形式的IPC。 本文将介绍上诉表中...

    目录

    一、管道(匿名管道)

    二、FIFO(命名管道)

    三、消息队列

    四、信号量

    五、共享内存


    Unix中的IPC(InterProcess Communication)是各种进程通信的统称,在Unix中有很多线程间通信方法,但是他们并不是兼容Unix的各种实现,下图列出了Linux系统不同实现所支持的不同形式的IPC。

    本文将介绍上诉表中比较经典的IPC:管道(匿名管道)、FIFO(命名管道)、消息队列、信号量、共享内存

    一、管道(匿名管道)

    1.什么是管道?

    把一个进程连接到另一个进程的一个数据流称为一个“管道”,通常是用作把一个进程的输出通过管道连接到另一个进程的输入。管道本质上是内核的一块缓存,管道是Unix IPC的最老形式,而且所有的Unix系统都支持这种通信机制。

    2.管道的特点

    (1)管道是半双工的,任意时刻数据只能在一个方向上流动;

    (2)管道只能在具有公共祖先的进程(父进程与子进程,或者两个兄弟进程)之间使用;;

    (3)管道的生命周期随进程,由进程创建,进程终止,管道占用的内存也随之归还给操作系统;

    (4)内核对管道操作进行同步与互斥,读完了就不读了,写满了就不写了,这种访问方式是典型的“生产者——消费者”模型;

    (5)管道面向字节流,传输的数据时没有数据结构的字节流。

    3.管道的一端被关闭,下列规则起作用:

    (1)当读一个写端已被关闭的管道时,在所有数据都被读取后,,read返回0,以指示达到了文件结束处;

    (2)如果写一个读端已被关闭的管道,则产生信号SIGPIPE。如果忽略该信号或者捕捉该信号并从其处理程序返回,则write出错返回,进程终止,ernno置为EPIPE

    4.管道的创建

    #include<unistd.h>
    int pipe(int fd[2]);   //成功0,失败-1

    参数 fd[2]
    输出型参数,接收两个打开的文件描述符
    fd[0] 为读打开
    fd[1] 为写打开

    如创建从父进程到子进程的管道,父进程从 fd[1] 写入,子进程从 fd[0]读出,fd[1]的输出作为fd[0]的输入。

    返回值
    若成功,返回 0
    若出错,返回 -1

    管道的数据在内核(内存)中流动

    5.管道的用法

    单个进程中的管道几乎没有任何用处。通常,调用 pipe的进程接着调用fork,这样就创建,了从父进程到子进程或反之的IPC通道。

    (1)对于从父进程到子进程的管道,父进程关闭管道的读端(fd [0]),子进程则关闭写端(fd [1])

    (2)对于从子进程到父进程的管道,父进程关闭管道的写端(fd [1]),子进程则关闭读端(fd [0])

    6.实例

    创建一个从父进程到子进程的管道,并且父进程经过管道向子进程传送数据;

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/wait.h>
    int main()
    {
    	int fd[2]={0};
    
    	if(pipe(fd)<0)   //创建管道(成功0,失败-1)
    	{
    		perror("pipe error!\n");
    		exit(1);
    	}
    	pid_t pid = fork();   //创建子进程
    
    	if(pid>0)  //父进程
    	{
    		close(fd[0]);  //父进程关闭读端
    		const char*msg="cat,I am a dog!\n";
    		write(fd[1],msg,strlen(msg));
    		wait(NULL);
    	}
    
    	if(pid==0) //子进程
    	{
    		close(fd[1]);  //子进程关闭写端
    		char buf[1024] = {0};
    		read(fd[0],buf,1024);  //阻塞
    		printf("father pipe:%s",buf);
    	}
    	
    	return 0;
    }
    

    运行结果:

     7.popen和pclose函数

    使用常见的操作就是,创建一个连接到另一个进程的管道,然后读其输出或向输入端发送数据,所以标准 I/O库提供了两个函数 popen 和 pclose。这两个函数的操作是:创建一个管道, fork 一个子进程,关闭未使用的管道端,执行一个 shell 命令,然后等待命令终止。

    #include<stdio.h>
    FILE *popen(const char*cmdstring,const char* type); //返回:成功则返回文件指针,失败则为NULL
    int pclose(FILE fp*) //返回:cmdstring终止状态,若出错则返回-1

    函数 popen 先执行 fork,然后调用 exec 执行 cmdstring,并且返回一个标准 I/O 文件。
    如果 type 是 “r”, 则文件连接到 cmdstring 的标准输出;
    如果 type 是 “w”,则文件链接到 cmdstring 的标准输入

    pclose 函数关闭标准 I/O 流,等待命令终止,然后返回 shell 的终止状态。

    图像 小部件

     popen和pclose函数是对前面讲述的函数的封装,可自行实现。

    二、FIFO(命名管道)

    1.什么是FIFO

    FIFO有时被称为命名管道。管道只能由具有血缘关系的进程使用,它们共同的祖先进程创建了管道。但是,通过FIFO,不具有血缘关系的进程也能交换数据。

    FIFO是一种文件类型,创建FIFO类似于创建文件,而且FIFO的路径名存在与文件系统中,通过stat结构成员st_mode的编码指明文件是否为FIFO类型,可以用S_ISFIFO宏对此进行测试。

    2.创建FIFO

    (1) 在控制台,通过mkfifo命令直接创建

     (2)在程序中通过mkfifo函数创建

    #include<sys/types.h>
    #include<sys/stat.h>
    int mkfifo(const char *pathname,mode_t mode);  // 成功为0,失败为-1

    特别说明:mkfifo只创建管道,不打开,一旦已经用mkfifo创建了一个FIFO(mode参数和open函数一样),就可用open打开它,而且一般的文件 I / O函数(close、read、write、unlink等)都可用于FIFO; 

    path 参数
    指明路径
    mode 参数
    这里要注意一下,创建的管道文件的权限是 mode & ~umask
    创建屏蔽字umask可以通过 umask 函数调整,例如 umask(0000)
    mode与打开普通文件的 open() 函数中的 mode 参数相同

    返回值
    若成功,返回 0
    若出错,返回 -1

    3.FIFO的注意事项

    当 open 一个 FIFO 时,非阻塞标志(O_NONBLOCK)会产生下列影响(阻塞与非阻塞模式)

    (1) 没有指定 O_NONBLOCK ,只读 open 要阻塞到某个其他进程为写而打开这个 FIFO 为止;只写 open 要阻塞到某个其他进程为读而打开它为止。

    (2) 如果指定了 O_NONBLOCK,则只读 open 立即返回。但是,如果没有进程为读而打开这个 FIFO,那么只写 open 将返回 -1,并将 errno 设置成 ENXIO。

    类似于管道,若 write 一个尚无进程为读而打开的 FIFO ,则产生 SIGPIPE 信号,若忽略此信号则进程终止;若某个 FIFO 的最后一个写进程关闭了该 FIFO,则将为该 FIFO 的读进程产生一个文件结束标志。

    一个给定的 FIFO 有多个写进程是常见的。如果不希望多个进程所写的数据交叉,则必须考虑原子写操作:常数PIPE_BUF说明了可被原子写到FIFO的最大数据量,每次写入的长度要小于PIPE_BUF字节,这样就能避免多个进程各次写之间的穿插

    4.FIFO的用途

    (1) shell 命令使用 FIFO 将数据从一条管道传送到另一条时,无需创建临时文件;

    (2) 客户进程-服务器进程应用程序中,FIFO 用作汇聚点,在客户进程和服务器进程之间传递数据(如图)。

    5.实例

    我们写一个简单的程序:创建一个命名管道,writer.c 以 只写方式打开 FIFO, 并获取键盘输入数据写到管道,reader.c 以只读方式打开这个 FIFO,并把读到的数据打印到显示器上。

    (1)wirte.cpp

    #include<unistd.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    #include<sys/stat.h>
    #include<errno.h>
    #include<string.h>
    int main()
    {
    	const char* pathname="./myfifo";
    	umask(000);
    	int fd = mkfifo(pathname,00777);  //mode & ~umask 创建FIFO
    	if(fd<0)
    	{
    		if(errno == EEXIST)
    		{
    			printf("myfifo has been created,just use it\n");
    		}
    		else
    		{
    			perror("mkfifo error");
    			exit(-1);
    		}
    	}
    	fd = open("./myfifo",O_WRONLY);   //打开FIFO
    	if(fd<0)
    	{
    		perror("open error");
    	}
    	while(1)
    	{
    		char buf[1024]={0};
    		printf("> ");
    		fflush(stdout);
    		ssize_t ret_read = read(STDIN_FILENO,buf,sizeof(buf)-1);
    		if(ret_read<0)
    		{
    			perror("read error");
    			exit(-1);
    		}
    		else if(ret_read==0)  //终端输入关闭
    		{
    			printf("write done");
    			return 0;
    		}
    		else  //正常接受
    		{
    			ssize_t ret_write = write(fd,buf,strlen(buf));  //向FIFO中输入,阻塞状态
    			if(ret_write!=strlen(buf))
    			{
    				perror("write error");
    			}
    		}
    	}
    	return 0;
    }
    

    (2)read.cpp

    #include<unistd.h>
    #include<stdio.h>
    #include<stdlib.h>
    #include<sys/types.h>
    #include<sys/stat.h>
    #include<fcntl.h>
    #include<sys/stat.h>
    #include<errno.h>
    #include<string.h>
    int main()
    {
    	const char* pathname="./myfifo";
    	umask(000);
    	int fd = mkfifo(pathname,00777);  //mode & ~umask 创建FIFO
    	if(fd<0)
    	{
    		if(errno == EEXIST)
    		{
    			printf("myfifo has been created,just use it\n");
    		}
    		else
    		{
    			perror("mkfifo error");
    			exit(-1);
    		}
    	}
    	fd = open("./myfifo",O_RDONLY);   //打开FIFO
    	if(fd<0)
    	{
    		perror("open error");
    	}
    	while(1)
    	{
    		char buf[1024]={0};
    		ssize_t ret_read = read(fd,buf,sizeof(buf)-1);   //开始读FIFO,阻塞
    		if(ret_read<0)
    		{
    			perror("read error");
    			exit(-1);
    		}
    		else if(ret_read==0)  //终端输入关闭
    		{
    			printf("write done\n");
    			return 0;
    		}
    		else  //正常接受
    		{
    			ssize_t ret_write = write(STDOUT_FILENO,buf,strlen(buf));  //向FIFO中输入,阻塞状态
    			if(ret_write!=strlen(buf))
    			{
    				perror("write error");
    			}
    		}
    	}
    	return 0;
    }
    

     打开两个终端,然后分别运行write和read程序,即实现了这两个进程的通信,最后write收到控制台crit+c,进程结束,关闭了FIFO的写端口,则read进程的read()函数收到0,感受到了存在0个写端,程序退出。

    三、消息队列

    1.什么是消息队列

    消息队列是消息的链表,存放在内核中并由消息队列标识符标识,内核管理,我们只需要调用内核提供给我们的接口就行。

    我们可以将内核中的某个特定的消息队列画为一个消息链表,如图假设有一个具有三个消息的队列,消息长度分别为1字节,2字节和3字节,而且这些消息就是以这样的顺序写入该队列的。再假设这三个消息的类型分别为100,200,300.

    在bash终端可以使用 ipcs -q 查看内核现有队列,使用ipcrm -q msqid 删除指定队列。

    2.消息队列的特点

    (1)管道和FIFO都是随进程持续的,SYSTEM V IPC(消息队列、信号量、共享内存)都是随内核持续的。当一个管道或FIFO的最后一次关闭发生时,仍在该管道或FIFO上的数据将被丢弃。消息队列,除非内核自举或显式删除,否则其一直存在。在某个进程往一个队列写入消息之前,并不需要另外某个进程在该队列上等待消息的到达(和管道、FIFO不同)。

    (2)消息队列是双向通信的,通过消息的类型可以标识是服务器要读的还是客户机要读的,或者是那个客户机插入的等。

    (3)消息队列具有一定的先入先出特性,但是它可以实现消息的非先入先出查询;

    3.标识符和关键字

    标识符:每个内核中的 IPC结构(消息队列、信号量或共享存储段)都用一个非负整数的标识符( identifier )加以引用。当一个 IPC 结构被创建,然后又被删除时,与这种结构相关的标识符连续加 1,直到达到一个整型数的最大正值,然后又回转到0。

    关键字:标识符是IPC对象的内部名。为使多个合作进程能够在同一 IPC 对象上汇聚,需要提供一个外部命名方案,为此,每个 IPC 对象都和与一个键相关联,将这个键作为该对象的外部名。无论何时创建一个 IPC 结构,都应指定一个键,这个键的数据类型是基本系统数据类型 key_t,通常在 <sys/types.h>中被定义为长整型。

    可以调用函数 ftok 生成一个键,ftok提供的唯一服务就是由一个路径名和课题 ID 产生一个关键字。

    #include <sys/ipc.h>
    key_t ftok(const char * path, int id);  //若成功,返回键;若出错,返回 (key_t) - 1,path 参数必须引用一个现有文件

    4.创建或打开一个消息队列msgget

    #include <sys/msg.h>
    int msgget(key_t key, int msgflg);
    //返回值是一个整数标识符msgid,其他三个msg函数就用它来指代该队列。它是基于指定的key产生的,而key既可以是ftok的返回值,也可以是常值IPC_PRIVATE。

    key
    就是我们刚刚说的键了,key既可以是ftok的返回值,也可以是常值IPC_PRIVATE。

    msgflg
    是读写权限值的组合。它还可以与IPC_CREAT或IPC_CREAT | IPC_EXCL按位或,IPC_NOWAIT --- 读写消息队列要求无法得到满足时,不阻塞。

    每个消息队列都有一个 msqid_ds 结构与其关联,这个结构定义了队列的当前状态。
    struct msqid_ds {
    struct ipc_perm msg_perm; /* ipc_perm 结构 */
    msgqnum_t msg_qnum; /* 队列的消息条数 */
    msglen_t msg_qbytes; /* 最大消息占用字节数 */
    pid_t msg_lspid; /* 最后一条发送消息的进程 ID */
    pid_t msg_lrpid; /* 最后一条接收消息的进程 ID */
    time_t msg_stime; /* last-msgsnd() time */
    time_t msg_rtime; /* last-msgrcv() time */
    time_t msg_ctime; /* last-change time */
    ...
    }; 

    当创建一个新消息队列时,msqid_ds结构的如下成员被初始化。
    (1) msg_perm结构的uid和cuid成员被设置成当前进程的有效用户ID,gid和cgid成员被设置成当前进程的有效组ID。
    (2) msgflg中的读写权限位存放在msg_perm.mode中。
    (3) msg_qnum,msg_lspid,msg_lrpid,msg_stime和msg_rtime被置为0.
    (4) msg_ctime被设置成当前时间。
    (5) msg_qbytes被设置成系统限制值。
    (6) msg_qbytes被设置成系统限制值。

    4.对队列执行多种操作msgctl 

    msgctl函数提供在一个消息队列上的各种控制操作。

    #include<sys/msg.h>
    
    int msgctl(int msgid,int cmd,struct msqid_d *buf);   //成功为0 失败为-1

    msqid 是 msgget 的返回值

    cmd 参数指定 msqid 指定队列要执行的命令
    IPC_STAT:取此消息队列的 msqid_ds 结构,并将它存放在 buf 指向的结构中;
    IPC_SET:根据buf指向结构的值,设置此队列的 msqid_ds 结构中的 msg_perm.uid、msg_perm.gid、msg_perm.mode 和 msg_qbytes 字段(此命令要求有效ID或者超级用户);
    IPC_RMID:从系统中删除该消息队列以及仍在该队列中的所有数据。

    5. 调用 msgsnd 将数据放到消息队列中

    #include<sys/msg.h>
    
    int msgsnd(int msgid,const void *ptr,size_t nbytes,int flag);   //成功为0 失败为-1

    消息组成:消息都由三部分组成,它们是:正长整型类型字段、非负长度(nbytes)以及实际数据字节(对应于长度)。消息总是放在队列尾端。 

    这个函数设计得确实巧妙,巧妙在第2个参数ptr,通过一个参数知道了消息类型和实际值(配合ntypes)

    msqid 是 msgget 的返回值

    ptr 是一个数据块指针,指向一个结构体mymsg;
    struct mymsg {
    long mtype; /* 消息类型. 必须大于 0*/
    char mtext[1]; /* Message text. */
    }    //这个结构体的第二个字段可以根据自己需要进行调整;
    我们知道,结构体的地址和结构体第一个字段的地址相同,我们也可以说,这个 msgp 指向一个长整数,这个长整数是这条消息的类型;

    nbytes指明mymsg结构体第二个字段的大小,即消息数据的实际大小;

    msgflag 的值可以指定为 IPC_NOWAIT, 设置非阻塞
    如果消息队列已满(或者是队列中的消息总数等于系统限制值,或队列中的字节总数等于系统限制值),msgsnd 立即出错返回 EAGAIN如果没有设置 IPC_NOWAIT,进程会一直阻塞到:
    (1) 有空间可以容纳要发送的消息;
    (2) 从系统中删除了此消息队列;返回 EIDRM 错误
    (3) 捕捉到一个信号,并从信号处理程序返回。返回 EINTE 错误

    如果从系统中删除某个消息队列:没有维护引用计数,删了就删了使用这一消息队列的进程下次将出错返回。

    6.msgrcv从队列中取用消息

    #include<sys/msg.h>
    
    int msgrcv(int msqid,void * ptr,size_t nbytes,long type, int flag);  //成功则返回消息数据部分的长度,出错则返回-1

    msqid 是 msgget 的返回值

    ptr指定所接收消息的存放位置。

    nbytes指定了数据部分大小(只想要多长的数据),如果返回的消息长度大于 nbytes,并且 flag 设置了MSG_NOERROR,
    消息将被截断。

    type指定我们希望从队列中去除什么类型的消息(用它来进行非先进先出方式的读消息)
    type == 0 返回队列中的第一个消息
    type > 0  返回队列中消息类型为type的第一个消息
    type < 0  返回队列中消息类型值小于或等于type绝对值的消息,如果这种消息有若干个。则取类型值最小的消息。

    flag可以被设置为 IPC_NOWAIT ,使操作不阻塞
    如果没有所指定类型的消息,msgrcv 直接返回 -1,errno 设置为 ENOMSG
    如果没有指定 IPC_NOWAIT,则进程会一直阻塞到:
    (1) 有指定消息可用;
    (2) 从系统中删除此消息队列;
    (3) 捕捉到一个信号并从信号处理程序返回。

    注意:msgsnd 和 msgrcv 在执行成功之后,内核才会更新与该消息队列相关连的 msgid_ds 结构中的相关信息(调用者进程 ID,时间) 

    7.实例

    服务端(狗)和客户端(猫)通过指定的消息队列通信,一人说一句话轮询;

    (1)server.cpp

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/msg.h>
    #include<sys/ipc.h>
    #include<errno.h>
    
    //指定消息队列的KEY
    #define IPC_KEY 0x12345678
    
    //指定两种消息的类型,分别标识来自server和client
    #define TYPE_S 1
    #define TYPE_C 2
    
    //定义消息的结构(已经在msg.h进行了定义)
    struct msgbuff{
        long mtype;  //消息类型
        char mtext[512];  //消息的实体
    };
    
    int main()
    {
        int msgid = -1;
        //1.创建消息队列   msgget(key_t key , int msgflg);
        msgid = msgget(IPC_KEY,IPC_CREAT|0777);
        if(msgid<0)
        {
            perror("msgget error");
            exit(-1);
        }
        
        struct msgbuff buf;   
        while(1)
        {
            //2.开始从消息队列中读出消息
            memset(&buf,0x00,sizeof(struct msgbuff));   //首先清理
            msgrcv(msgid,&buf,sizeof(buf.mtext),TYPE_C,0);  //作为服务端,指向读出来自客户端的消息TYPE_C
            //打印读到的消息
            printf("[c]:%s\n",buf.mtext);
            //读取键盘输入
            memset(&buf,0x00,sizeof(msgbuff));
            buf.mtype=TYPE_S;
            printf("[s]>");
            fflush(stdout);
            scanf("%s",buf.mtext);
            //3.往消息队列插入消息
            msgsnd(msgid,&buf,strlen(buf.mtext),0);   
        }
        //4.删除消息队列
        msgctl(msgid,IPC_RMID,NULL);
    
        return 0;
    }                                                                                                                                              

    (2)client.cpp

    #include<stdio.h>                                                                                                                                                           
    #include<stdlib.h>
    #include<unistd.h>
    #include<string.h>
    #include<sys/types.h>
    #include<sys/msg.h>
    #include<sys/ipc.h>
    #include<errno.h>
    
    //指定消息队列的KEY
    #define IPC_KEY 0x12345678
     
    //指定两种消息的类型,分别标识来自server和client
    #define TYPE_S 1
    #define TYPE_C 2
     
    //定义消息的结构(已经在msg.h进行了定义)
    struct msgbuff{
      long mtype;  //消息类型
      char mtext[512];  //消息的实体
     };
    
    int main()
    {
        int msgid = -1;
        //1.打开消息队列   msgget(key_t key , int msgflg);
        msgid = msgget(IPC_KEY,0);
        if(msgid<0)
        {
            perror("msgget error");
            exit(-1);
        }
        
        struct msgbuff buf;   
        while(1)
        {
            //读键盘输入
            memset(&buf,0x00,sizeof(struct msgbuff));   //首先清理  
            printf("[c]>");
            fflush(stdout);
            scanf("%s",buf.mtext);
            //2.开始向消息队列中写入消息   
            buf.mtype = TYPE_C;
            msgsnd(msgid,&buf,strlen(buf.mtext),0);  //作为客户端端,想消息队列中插入的消息类型为TYPE_C
            //3.冲消息队列中读出来自服务器的消息
            memset(&buf,0x00,sizeof(struct msgbuff));  
            msgrcv(msgid,&buf,sizeof(buf.mtext),TYPE_S,0);  //作为客户端,希望读出来自服务端的消息 类型为TYPE_S
            //打印消息到终端
            printf("[S]:%s\n",buf.mtext);
        }
        //4.删除消息队列
        msgctl(msgid,IPC_RMID,NULL);
    
        return 0;
    }                                       
    

     编译运行,进行如下简单的会话。

     8.消息队列的缺陷(apue上说在新的应用程序中不应当再使用它们

    (1) 消息队列在系统中没有访问计数,不随进程的终止二消失,必须显示删除;

    (2)  IPC结构并不按名字为文件系统所知,不能使用通用函数来存取它们或修改它们的特性,为了操作消息队列,必须新定义很多个函数;

    四、信号量

    1.什么是信号量

    进程间通信方式之一,用于实现进程间同步与互斥(不传递数据)。多个进程同时操作一个临界资源的时候就需要通过同步与互斥机制来实现对临界资源的安全访问。

    信号量与前面介绍的管道、FIFO以及消息队列不同,信号量是具有一个等待队列的计数器(0代表现在还有没有资源可以使用)用于为多个进程提供对共享数据的访问

    常用的信号量形式被称之为双态信号量(binary semaphore)。它控制单个资源,其初始值为1。但是,一般而言,信号量的初值可以是任一正值,该值说明有多少个共享资源单位可供共享应用。SYSTEM V的信号量和POSIX的信号量思想相同,但是前者很复杂,而后者的使用非常简单,这里介绍的是SYSTEM V的信号量。

    2.实现方法

    当信号量没有资源可用时,这时候需要阻塞等待,常用的信号量形式被称之为双态信号量(binary semaphore)。它控制单个资源,其初始值为1。但是,一般而言,信号量的初值可以是任一正值,该值说明有多少个共享资源单位可供共享应用。

    同步:只有信号量资源计数大于0的时候,会通知别人,打断等待,去操作临界资源,也就是说,别人释放了资源(+1)之后你才能获取资源(-1)然后进行操作。

    互斥:信号量如果要实现互斥,那么它的计数只能是 0/1 (一元信号量,双态信号量),一个进程获取临界资源后,在他没释放临界资源之前,别的进程无法获取该临界资源。

    为了获取共享资源,进程需要执行下列操作:

    (1)测试控制该资源的信号量。

    (2)若此信号量的值为正(大于0),则进程可以使用该资源。在这种情况下,进程会将信号量值减一,表示使用了一个资源单位。

    (3)若此信号量值为0,则进程进入休眠状态,直至信号量大于0,进程被唤醒后,继续步骤(1)。

    当进程不再使用由一个信号量控制的共享资源时,该信号量值增1。如果有进程正在休眠等待此信号量,则唤醒他们。

    为了正确地实现信息量,信号量值的测试及减 1 操作是原子操作。为此,信号量通常是在内核中实现的。 

    3. 内核中信号量集合的结构

    内核为每个信号量集设置了一个 semid_ds结构
    struct semid_ds {
        struct ipc_permsem_perm ;
        structsem*    sem_base ; //信号数组指针
        ushort        sem_nsem ; //此集中信号个数
        time_t        sem_otime ; //最后一次semop时间
        time_t        sem_ctime ; //最后一次创建时间
    } ;

    某个指定的信号量的结构体
    struct sem {
        ushort_t  semval ;  //信号量的值
        short     sempid ;  //最后一个调用semop的进程ID
        ushort    semncnt ; //等待该信号量值大于当前值的进程数(一有进程释放资源 就被唤醒)
        ushort    semzcnt ; //等待该信号量值等于0的进程数
    } ; 

    4.创建或打开一个信号量 semget

    #include <sys/sem.h>
    int semget(ket_t key, int nsems, int flag); //成功则返回信号量ID,出错则返回-1

    key
    关键字,可以通过函数 ftok 创建,也可以自己指定

    nsems
    是该集合中的信号量数。如果是创建新集合(一般在服务器中),则必须指定nsems。
    如果引用一个现存的集合(一个客户机),则将nsems指定为0。

    oflag
    可以是SEM_R(read)和SEM_A(alter)常值的组合(打开时用到),也可以是IPC_CREAT或IPC_EXCL 。

    5.semctl 包含的多种信号量操作 

    #include <sys.sem.h>
    int semctl(int semid, int semnum, int cmd, union semun arg );

    semid
    指定的信号量集合

    semnum
    指定该集合中的一个成员,取值范围:[0, nsems)

    cmd
    指定下列 10 种命令中的一种,使其在semid指定的信号量集合上执行此命令。其中有五条命令是针对一个特定的信号量值的,它们用semnum指定该集合中的一个成员。
    IPC_STAT    对此集合取 semid_ds 结构,存储在 arg.buf 指向的结构中
    IPC_SET    按 arg.buf 指向的结构中的值,设置与此集合相关的 semid_ds 结构
    IPC_RMID    从系统中删除该信号量集合
    GETVAL    返回成员 semnum 的 semval 值
    SETVAL    设置成员 semnum 的 semval 值,该值由 arg.val 指定
    GETPID    返回成员 semnum 的 sempid 值
    GETNCNT    返回成员 semnum 的 semncnt 值
    GETZCNT    返回成员 semnum 的 semzcnt 值
    GETALL    取该集合中所有的信号量值,这些值存储在 arg.array 指向的数组中
    SETALL    将集合中所有的信号量设置成 arg.array 指向的数组中的值

    arg
    可选,是否使用取决于所请求命令
    如果使用该参数,则其类型是 semun,是多个命令特定参数的联合(union):
    union semun {
    int val;
    struct semid_ds * buf;
    unsigned short * array;
    }; 这个选项参数是一个联合,而非指向联合的指针

    返回值
    对 GETALL 以外的所有 GET 命令,semctl 函数都返回相应值
    对其他命令:
    若成功,返回 0
    若出错,设置 errno 并返回 -1

    6.semop自动执行信号量集合上的操作数组

    #include <sys/sem.h>
    int semop(int semid, struct sembuf semoparray[], size_t nops);  //成功返回0 失败返回-1

    semop 函数具有原子性,要么执行数组中的所有操作,要么一个也不做。

    semid
    指定的信号量集合

    semoparray
    是一个指针,指向一个由 sembuf 结构表示的信号量操作数组:
    struct sembuf {
    unsigned short sem_num;
    short sem_op;
    short sem_flg;
    };

    nops
    规定该数组中操作的数量,对集合中每个成员的操作由相应的 sem_op 值规定
    sem_op 值可以是负值、0或正值
    信号量的 “undo” 标志,此标志对应于 sem_flg 成员的 SEM_UNDO 位
    (1)sem_op 为正值,表示的是进程释放的占用资源数,sem_op 值会加到信号量的值上,如果指定了 undo 标志,则也从该进程的此信号量调整值中减去 sem_op 。
    (2)sem_op 为负值,表示要获取由该信号量控制的资源
    如若该信号量的值大于等于 sem_op 的绝对值,则从信号量中减去 sem_op 的绝对值。
    如若信号量小于 sem_op 的绝对值:
    a. 若指定了 IPC_NOWAIT,则 semop 出错返回 EAGAIN ;
    b. 若未指定 IPC_NOWAIT,则该信号量的 semncnt 加 1,然后调用进程被挂起等待直至下列事件之一发生:
    i. 此信号量值变为大于等于 sem_op 的绝对值
    ii. 从系统中删除了此信号量
    iii. 进程捕捉到一个信号,并从信号处理程序返回
    (3)若 sem_op 为 0,这表示调用进程希望等待到该信号量变为 0.
    若信号量值当前为 0 , 则此函数立即返回。
    如果此信号量值非 0,则适用于下列条件:
    a. 若指定了 IPC_NAWAIT,则出错返回 EAGAIN
    b. 若未指定 IPC_NOWAIT,则该信号量的 semzcnt 值加 1,然后调用进程被挂起,直至下列的一个事件发生。
    i. 此信号量值变为 0。此信号量的 semzcnt 值减 1。
    ii. 从系统中删除了此信号量。
    iii. 进程捕捉到一个信号,并从信号处理程序返回。在这种情况下,此信号量的 semzcnt 值减 1,并且函数出错返回 EINTR。

    7. exit 时的信号调整

    一个进程终止时,如果它占用了经由信号量分配的资源(执行P操作,减1),并且没有归还给(执行V 操作,加1),等待的其他进程将会一直阻塞,那么就会出现问题。这也是信号量必须处理的问题,它是这样做的:

    无论何时只要为信号量操作指定了 SEM_UNDO 标志,然后分配资源(sem_op 值小于 0),那么内核就会记住对于该特定信号量,分配给调用进程多少资源(sem_op 的绝对值)。所以设置SEM_UNDO是很有用途的。

    当该进程终止时,不论自愿或不自愿内核都将检验该进程是否还有尚未处理的信号量调整值,如果有,则按调整值对相应信号量进行处理。如果用带SETVAL 或SETALL命令的 semctl 设置一个信号量的值,则在所有进程中,该信号量的调整值都将设置为 0。

    8.实例

    信号量同步操作:访问资源的时序性
    一个简单的生产消费模型
    买方便面的爸爸:生产者(每隔一秒生产一包方便面)
    吃方便面的儿子:消费者
    刚开始是没有方便面的,只有生产者生产出来方便面,消费者才能消费、

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/sem.h>
    #include<sys/ipc.h>
    #include <sys/wait.h>
    //指定信号量的KEY
    #define IPC_KEY 0x12345678 
    
    union semun
    {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
        struct seminfo *_buf;
    };
    
    void sem_P(int id)  //信号量P操作
    {
        struct sembuf buf;
        buf.sem_num=0;      //要操作的指定信号量
        buf.sem_op = -1;    //具体操作值(P 操作-1)
        buf.sem_flg = SEM_UNDO;  //SEM_UNDO:当程序异常终止时,避免产生死锁
    
        semop(id,&buf,1);   //只操作信号量集合中的一个指定的信号量
    }
    
    void sem_V(int id)  //信号量P操作
    {
        struct sembuf buf;   //要操作的指定信号量
        buf.sem_num=0;
        buf.sem_op = 1;      //具体操作值(V 操作1)
        buf.sem_flg=SEM_UNDO; //SEM_UNDO:当程序异常终止时,避免产生死锁
    
        semop(id,&buf,1);    //只操作信号量集合中的一个指定的信号量
    }
    
    int main()
    {
        int pid = 0;
    
        //1.创建或打开一个信号量
        int semid =semget(IPC_KEY,1,IPC_CREAT|0777);  //使用指定KEY创建包含一个信号量的信号量集合
        if (semid<0) {
            perror("semger error!");
            exit(1);
        }
    
        //2. 设置信号量初值,只能设置1次,不能重复设置
        union semun un_sem;
        un_sem.val = 0;
        semctl(semid,0,SETVAL,un_sem);  //设置信号量集合中0号信号量的初值为0
        pid = fork();
    
        if(pid<0)
        {
            perror("fork error");
            exit(2);
        } 
        else if(pid==0)   //子进程(吃方便面)
        {
            while(1)
            {
                sem_P(semid);   //P操作,信号量-1
                printf("儿子:我吃了一包方便面!\n");
    
            }
    
        }
        else   //父进程(买方便面)
        {
            while(1)
            {
                sleep(1);
                sem_V(semid);   //V操作,信号量+1
                printf("爸爸:我买了一包方便面!\n");
            }
            wait(NULL);
        }
        return 0;
    }

     编译运行,可见实现了两个进程间的同步(生产者生产之后消费者才能消费)

    互斥操作:进程之间的代码段具有排他性,不能同时执行
    让一个进程打印A, 睡1S ,然后再打印一个A
    让另一个进程打印B,睡 3S ,
    然后再打印一个B
    检查结果是否出现A与B的交叉 

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/sem.h>
    #include<sys/ipc.h>
    #include <sys/wait.h>
    //指定信号量的KEY
    #define IPC_KEY 0x12345678 
    
    union semun
    {
        int val;
        struct semid_ds *buf;
        unsigned short *array;
        struct seminfo *_buf;
    };
    
    void sem_P(int id)  //信号量P操作
    {
        struct sembuf buf;
        buf.sem_num=0;      //要操作的指定信号量
        buf.sem_op = -1;    //具体操作值(P 操作-1)
        buf.sem_flg = SEM_UNDO;  //SEM_UNDO:当程序异常终止时,避免产生死锁
    
        semop(id,&buf,1);   //只操作信号量集合中的一个指定的信号量
    }
    
    void sem_V(int id)  //信号量P操作
    {
        struct sembuf buf;   //要操作的指定信号量
        buf.sem_num=0;
        buf.sem_op = 1;      //具体操作值(V 操作1)
        buf.sem_flg=SEM_UNDO; //SEM_UNDO:当程序异常终止时,避免产生死锁
    
        semop(id,&buf,1);    //只操作信号量集合中的一个指定的信号量
    }
    
    int main()
    {
        int pid = 0;
    
        //1.创建或打开一个信号量
        int semid =semget(IPC_KEY,1,IPC_CREAT|0777);  //使用指定KEY创建包含一个信号量的信号量集合
        if (semid<0) {
            perror("semger error!");
            exit(1);
        }
    
        //2. 设置信号量初值,只能设置1次,不能重复设置
        union semun un_sem;
        un_sem.val = 1;
        semctl(semid,0,SETVAL,un_sem);  //设置信号量集合中0号信号量的初值为1
        pid = fork();
    
        if(pid<0)
        {
            perror("fork error");
            exit(2);
        } 
        else if(pid==0)   //子进程 打印A 睡1s 打印A
        {
            while(1)
            {
                sem_P(semid);   //P操作,获取资源,信号量-1
                printf("A");
                fflush(stdout);
                sleep(1);
                printf("A\n");
                fflush(stdout);
                sem_V(semid);  //V操作,释放资源,信号量+1
            }
    
        }
        else   //父进程 打印B 睡3s 打印B
        {
            while(1)
            {
                sem_P(semid);   //P操作,获取资源,信号量-1
                printf("B");
                fflush(stdout);
                sleep(3);
                printf("B\n");
                fflush(stdout);
                sem_V(semid);  //V操作,释放资源,信号量+1
            }
            wait(NULL);
        }
        
        return 0;
    }

    编译运行,可见两个进程的打印功能的程序段确实是互斥的。

    五、共享内存

    1.什么是共享内存

    共享存储允许两个或多个进程共享一给定的存储区。因为数据不需要在客户机和服务器之间复制,所以这是最快的一种 I P C(读写同一块物理内存)。

    å±äº«åå­åç

    2.如何实现

    使用共享存储的唯一窍门是多个进程之间对一给定存储区的同步存取。若服务器将数据放入共享存储区,则在服务器做完这一操作之前,客户机不应当去取这些数据。通常,信号量被用来实现对共享存储存取的同步。(记录锁/读写锁也可用于这种场合。)

    3.内核中共享内存的属性结构

    struct shmid_ds {
      struct ipc_perm shm_perm; /* ipc_perm 结构 */
      struct anon_map *shm_amp;  /*pointer in kernel*/
      int shm_segsz; /* size of segment in bytes */  
      ushort shm_lkcnt; /* number of times segment is being locked*/
      pid_t shm_lpid; /* pid of last shmop() */
      pid_t shm_cpid; /* pid of creator */
      ulong shm_nattch; /* number of current attaches */  
      ulong shm_cnattach; /* used only for shminfo*/
      time_t shm_atime; /* last-attach time */
      time_t shm_dtime; /* last-detach time */
      time_t shm_ctime; /* last-change time */
      ...
     };

    4.shmget 函数创建共享内存或获得一个共享内存标识符

    #include<sys/shm.h>
    int shmget(key_t key,int size,int flag);  //成功返回共享内存ID,出错返回-1

    key
    关键字,可以通过函数 ftok 创建,也可以自己指定

    size
    是该共享存储段的最小值。如果正在创建一个新段(一般在服务器中),则必须指定其size。如果正在存访一个现存的段(一个客户机),则将size指定为0。

    oflag
    读写权限值的组合

    5.shmctl 函数对共享内存执行多种操作

    #include <sys/shm.h>
    int shmctl(int shmid, int cmd, struct shmid_ds * buf); //成功为0,出错为-1

    shmid
    shmget 函数返回的共享内存标识符

    cmd
    指定下列 5 种命令中的一种,使其在 shmid 指定的段上执行
    IPC_STAT        取此段的 shmid_ds 结构,并将它存储在由 buf 指向的结构中
    IPC_SET         将 buf 指向的结构中的值设置到此共享存储段的 shmid_ds 结构
    IPC_RMID       从系统中删除该共享存储段
    IPC_LOCK      在内存中对共享存储段加锁
    IPC_UNLOCK    解锁共享存储段

    6.shmat 函数将共享内存连接到调用进程的地址空间中

    #include <sys/shm.h>
    void * shmat(int shmid, const void * addr, int flag); //成功则返回执行共享内存段的指针,出错则为-1

    shmid
    shmget 函数返回的共享内存标识符

    addr
    共享存储段连接到调用进程的哪个地址上与 addr 参数以及 flag 中是否指定 SHM_RND 位有关。
    SHM_RND 命令的意思是 “取整”;
    如果 addr 为 0,则此段连接到由内核选择的第一个可用地址上(一般应指定 addr为0,以便由内核选择地址。
    如果 addr 非 0,并且没有指定 SHM_RND ,则此段连接到 addr 所指定的地址上;
    如果 addr 非 0,并且指定了 SHM_RND,则此段连接到 (addr - (addr mod SHMLBA)) 所表示的地址上,该算式是将地址向下取最近 1 个 SHMLBA 的倍数。
    SHMLBA 的意思是 “低边界地址倍数”,它总是 2 的乘方。

    参数 flag
    如果指定了 SHM_RDONLY 位,则以只读方式连接此段,否则以读写方式连接此段;
    如果 shmat 执行成功,那么内将与该共享存储段相关的 shmid_ds 结构中的 shm_nattch 计数器加 1。

    7.函数 shmdt 使共享存储段与该进程分离

    #include <sys/shm.h>
    int shmdt(void * addr);      //成功返回0,出错返回-1

    addr
    调用 shmat 函数的返回值,共享内存在进程中的实际地址;

    如果执行成功, shmdt 将使相关的 shmid_ds 结构中的 shm_nattch 计数器减1。

    注意,这并不从系统中删除其标识符以及其数据结构。该标识符仍然存在,直至某个进程(一般是服务器)调用 shmctl
    (带命令IPC_RMID)特地删除它。

    8.实例

    这是一个基于共享内存的聊天程序的服务器端
    共享内存操作步骤:

    1.创建共享内存
    2.映射共享内存到虚拟地址空间
    3.多线程同步读写(一般任务记得进行同步和互斥操作
    4.通信结束(解除映射关系,删除共享内存)

     (1)writer.cpp

    writer.cpp负责创建共享内存,读取键盘输入,然写入共享内存。

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/shm.h>
    #include<sys/ipc.h>
    #include <sys/wait.h>
    //指定信号量的KEY
    #define IPC_KEY 0x12345678 
    
    int main()
    {
        //1.创建共享内存
        int shmid = shmget(IPC_KEY,512,IPC_CREAT|0777);
        if(shmid<0)
        {
            perror("shmget error");
            exit(1);
        }
        printf("success get sharememory\n");
        //2.将共享内存连接到虚拟内存空间
        void *shm_ptr = shmat(shmid,0,0);   //设置addr为0,让内核选择虚拟地址空间中的第一个可用地址
        if(shm_ptr == (void *)-1)
        {
            perror("shmat error");
            exit(2);
        }
        //3.往共享内存写入数据
        while(1)
        {
            printf(">");
            fflush(stdout);
            scanf("%s",(char *)shm_ptr);
        }
        //4.解除进程与共享内存连接
        shmdt(shm_ptr);
        //5.删除共享内存
        shmctl(shmid,IPC_RMID,NULL);
        return 0;
    }       

    (2)reader.cpp

    #include<stdio.h>
    #include<stdlib.h>
    #include<unistd.h>
    #include<sys/types.h>
    #include<sys/shm.h>
    #include<sys/ipc.h>
    #include<sys/wait.h>
    #include<string.h>
    //指定信号量的KEY
    #define IPC_KEY 0x12345678 
    
    int main()
    {
        //1.获取共享内存标识符
        int shmid = shmget(IPC_KEY,0,0777);
        if(shmid<0)
        {
            perror("shmget error");
            exit(1);
        }
        printf("success get sharememory\n");
        //2.将共享内存连接到虚拟内存空间
        void *shm_ptr = shmat(shmid,0,0);   //设置addr为0,让你内核选择虚拟地址空间中的第一个可用地址
        if(shm_ptr == (void *)-1)
        {
            perror("shmat error");
            exit(2);
        }
        //3.从共享内存读出数据
        while(1)
        {
            if(strlen((char *)shm_ptr))
            {
                printf("read: %s\n",(char *)shm_ptr);
                memset(shm_ptr,0x00,512);  //清理内存
            }
            sleep(1);
        }
        //4.解除进程与共享内存连接
        shmdt(shm_ptr);
        //5.删除共享内存
        shmctl(shmid,IPC_RMID,NULL);
        return 0;
    }

    reader.cpp负责获取共享内存标识符,从共享内存中读取内容,输出到标准输出。 

     编译运行,可以看到两个进程通过共享内存进行数据传输,当然这个比较简单,一般的任务都需要信号量等机制做进程间的同步或互斥。

    9.关于删除共享内存的问题 

    在删除共享内存的时候,并不是直接删的。如果有进程依然与共享内存保持映射连接关系,那么共享内存将不会被立即删除,而是等最后一个映射断开后删除,在这期间,将拒绝其他进程使用shmat连接此共享内存!


    参考:

    1.《UNIX环境高级编程》

    2.博客:https://www.cnblogs.com/wangfengju/p/6172730.html

    3.博客:https://blog.csdn.net/yang_yulei/article/details/19772649

    4.博客:https://blog.csdn.net/eric_qiushui/article/details/83820562#_FIFO_134

    展开全文
  • 我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那...

           我们深谙信息交流的价值,那网络中进程之间如何通信,如我们每天打开浏览器浏览网页时,浏览器的进程怎么与web服务器通信的?当你用QQ聊天时,QQ进程怎么与服务器或你好友所在的QQ进程通信?这些都得靠socket?那什么是socket?socket的类型有哪些?还有socket的基本函数,这些都是本文想介绍的。本文的主要内容如下:

    • 1、网络中进程之间如何通信?
    • 2、Socket是什么?
    • 3、socket的基本操作
      • 3.1、socket()函数
      • 3.2、bind()函数
      • 3.3、listen()、connect()函数
      • 3.4、accept()函数
      • 3.5、read()、write()函数等
      • 3.6、close()函数
    • 4、socket中TCP的三次握手建立连接详解
    • 5、socket中TCP的四次握手释放连接详解

    1、网络中进程之间如何通信?

    本地的进程间通信(IPC)有很多种方式,但可以总结为下面4类:

    • 消息传递(管道、FIFO、消息队列)
    • 同步(互斥量、条件变量、读写锁、文件和写记录锁、信号量)
    • 共享内存(匿名的和具名的)
    • 远程过程调用(Solaris门和Sun RPC)

           但这些都不是本文的主题!我们要讨论的是网络中进程之间如何通信?首要解决的问题是如何唯一标识一个进程,否则通信无从谈起!在本地可以通过进程PID来唯一标识一个进程,但是在网络中这是行不通的。其实TCP/IP协议族已经帮我们解决了这个问题,网络层的“ip地址”可以唯一标识网络中的主机,而传输层的“协议+端口”可以唯一标识主机中的应用程序(进程)。这样利用三元组(ip地址,协议,端口)就可以标识网络的进程了,网络中的进程通信就可以利用这个标志与其它进程进行交互。

           使用TCP/IP协议的应用程序通常采用应用编程接口:UNIX  BSD的套接字(socket)和UNIX System V的TLI(已经被淘汰),来实现网络进程之间的通信。就目前而言,几乎所有的应用程序都是采用socket,而现在又是网络时代,网络中进程通信是无处不在,这就是我为什么说“一切皆socket”。


    2、什么是Socket?

           上面我们已经知道网络中的进程是通过socket来通信的,那什么是socket呢?socket起源于Unix,而Unix/Linux基本哲学之一就是“一切皆文件”,都可以用“打开open –> 读写write/read –> 关闭close”模式来操作。我的理解就是Socket就是该模式的一个实现,socket即是一种特殊的文件,一些socket函数就是对其进行的操作(读/写IO、打开、关闭),这些函数我们在后面进行介绍。


    3、socket的基本操作

           既然socket是“open—write/read—close”模式的一种实现,那么socket就提供了这些操作对应的函数接口。下面以TCP为例,介绍几个基本的socket接口函数。


    3.1、socket()函数

    int socket(int domain, int type, int protocol);

           socket函数对应于普通文件的打开操作。普通文件的打开操作返回一个文件描述字,而socket()用于创建一个socket描述符(socket descriptor),它唯一标识一个socket。这个socket描述字跟文件描述字一样,后续的操作都有用到它,把它作为参数,通过它来进行一些读写操作。

           正如可以给fopen的传入不同参数值,以打开不同的文件。创建socket的时候,也可以指定不同的参数创建不同的socket描述符,socket函数的三个参数分别为:

    • domain:即协议域,又称为协议族(family)。常用的协议族有,AF_INETAF_INET6AF_LOCAL(或称AF_UNIX,Unix域socket)、AF_ROUTE等等。协议族决定了socket的地址类型,在通信中必须采用对应的地址,如AF_INET决定了要用ipv4地址(32位的)与端口号(16位的)的组合、AF_UNIX决定了要用一个绝对路径名作为地址。
    • type:指定socket类型。常用的socket类型有,SOCK_STREAMSOCK_DGRAMSOCK_RAWSOCK_PACKETSOCK_SEQPACKET等等。
    • protocol:故名思意,就是指定协议。常用的协议有,IPPROTO_TCPIPPTOTO_UDPIPPROTO_SCTPIPPROTO_TIPC等,它们分别对应TCP传输协议、UDP传输协议、STCP传输协议、TIPC传输协议。

           注意:并不是上面的type和protocol可以随意组合的,如SOCK_STREAM不可以跟IPPROTO_UDP组合。当protocol为0时,会自动选择type类型对应的默认协议。

           当我们调用socket创建一个socket时,返回的socket描述字它存在于协议族(address family,AF_XXX)空间中,但没有一个具体的地址。如果想要给它赋值一个地址,就必须调用bind()函数,否则就当调用connect()listen()时系统会自动随机分配一个端口。


    3.2、bind()函数

           正如上面所说bind()函数把一个地址族中的特定地址赋给socket。例如对应AF_INETAF_INET6就是把一个ipv4或ipv6地址和端口号组合赋给socket。

    int bind(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

          函数的三个参数分别为:

    • sockfd:即socket描述字,它是通过socket()函数创建了,唯一标识一个socket。bind()函数就是将给这个描述字绑定一个名字。
    • addr:一个const struct sockaddr *指针,指向要绑定给sockfd的协议地址。这个地址结构根据地址创建socket时的地址协议族的不同而不同,如ipv4对应的是:
      struct sockaddr_in {
          sa_family_t    sin_family; /* address family: AF_INET */
          in_port_t      sin_port;   /* port in network byte order */
          struct in_addr sin_addr;   /* internet address */
      };
      
      /* Internet address. */
      struct in_addr {
          uint32_t       s_addr;     /* address in network byte order */
      };
      ipv6对应的是: 
      struct sockaddr_in6 { 
          sa_family_t     sin6_family;   /* AF_INET6 */ 
          in_port_t       sin6_port;     /* port number */ 
          uint32_t        sin6_flowinfo; /* IPv6 flow information */ 
          struct in6_addr sin6_addr;     /* IPv6 address */ 
          uint32_t        sin6_scope_id; /* Scope ID (new in 2.4) */ 
      };
      
      struct in6_addr { 
          unsigned char   s6_addr[16];   /* IPv6 address */ 
      };
      Unix域对应的是: 
      #define UNIX_PATH_MAX    108
      
      struct sockaddr_un { 
          sa_family_t sun_family;               /* AF_UNIX */ 
          char        sun_path[UNIX_PATH_MAX];  /* pathname */ 
      };
    • addrlen:对应的是地址的长度。

           通常服务器在启动的时候都会绑定一个众所周知的地址(如ip地址+端口号),用于提供服务,客户就可以通过它来接连服务器;而客户端就不用指定,有系统自动分配一个端口号和自身的ip地址组合。这就是为什么通常服务器端在listen之前会调用bind(),而客户端就不会调用,而是在connect()时由系统随机生成一个。


    网络字节序与主机字节序

    主机字节序就是我们平常说的大端和小端模式:不同的CPU有不同的字节序类型,这些字节序是指整数在内存中保存的顺序,这个叫做主机序。引用标准的Big-Endian和Little-Endian的定义如下:

      a) Little-Endian就是低位字节排放在内存的低地址端,高位字节排放在内存的高地址端。

      b) Big-Endian就是高位字节排放在内存的低地址端,低位字节排放在内存的高地址端。

    网络字节序:4个字节的32 bit值以下面的次序传输:首先是0~7bit,其次8~15bit,然后16~23bit,最后是24~31bit。这种传输次序称作大端字节序。由于TCP/IP首部中所有的二进制整数在网络中传输时都要求以这种次序,因此它又称作网络字节序。字节序,顾名思义字节的顺序,就是大于一个字节类型的数据在内存中的存放顺序,一个字节的数据没有顺序的问题了。

    所以:在将一个地址绑定到socket的时候,请先将主机字节序转换成为网络字节序,而不要假定主机字节序跟网络字节序一样使用的是Big-Endian。由于这个问题曾引发过血案!公司项目代码中由于存在这个问题,导致了很多莫名其妙的问题,所以请谨记对主机字节序不要做任何假定,务必将其转化为网络字节序再赋给socket。


    3.3、listen()、connect()函数

           如果作为一个服务器,在调用socket()bind()之后就会调用listen()来监听这个socket,如果客户端这时调用connect()发出连接请求,服务器端就会接收到这个请求。

    int listen(int sockfd, int backlog);
    int connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen);

           listen函数的第一个参数即为要监听的socket描述字,第二个参数为相应socket可以排队的最大连接个数。socket()函数创建的socket默认是一个主动类型的,listen函数将socket变为被动类型的,等待客户的连接请求。

           connect函数的第一个参数即为客户端的socket描述字,第二参数为服务器的socket地址,第三个参数为socket地址的长度。客户端通过调用connect函数来建立与TCP服务器的连接。


    3.4、accept()函数

           TCP服务器端依次调用socket()bind()listen()之后,就会监听指定的socket地址了。TCP客户端依次调用socket()connect()之后就想TCP服务器发送了一个连接请求。TCP服务器监听到这个请求之后,就会调用accept()函数取接收请求,这样连接就建立好了。之后就可以开始网络I/O操作了,即类同于普通文件的读写I/O操作。

    int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);

           accept函数的第一个参数为服务器的socket描述字,第二个参数为指向struct sockaddr *的指针,用于返回客户端的协议地址,第三个参数为协议地址的长度。如果accpet成功,那么其返回值是由内核自动生成的一个全新的描述字,代表与返回客户的TCP连接。

           注意accept的第一个参数为服务器的socket描述字,是服务器开始调用socket()函数生成的,称为监听socket描述字;而accept函数返回的是已连接的socket描述字。一个服务器通常通常仅仅只创建一个监听socket描述字,它在该服务器的生命周期内一直存在。内核为每个由服务器进程接受的客户连接创建了一个已连接socket描述字,当服务器完成了对某个客户的服务,相应的已连接socket描述字就被关闭。


    3.5、read()、write()等函数

           万事具备只欠东风,至此服务器与客户已经建立好连接了。可以调用网络I/O进行读写操作了,即实现了网咯中不同进程之间的通信!网络I/O操作有下面几组:

    • read()/write()
    • recv()/send()
    • readv()/writev()
    • recvmsg()/sendmsg()
    • recvfrom()/sendto()

           我推荐使用recvmsg()/sendmsg()函数,这两个函数是最通用的I/O函数,实际上可以把上面的其它函数都替换成这两个函数。它们的声明如下:

           #include <unistd.h>
    
           ssize_t read(int fd, void *buf, size_t count);
           ssize_t write(int fd, const void *buf, size_t count);
    
           #include <sys/types.h>
           #include <sys/socket.h>
    
           ssize_t send(int sockfd, const void *buf, size_t len, int flags);
           ssize_t recv(int sockfd, void *buf, size_t len, int flags);
    
           ssize_t sendto(int sockfd, const void *buf, size_t len, int flags,
                          const struct sockaddr *dest_addr, socklen_t addrlen);
           ssize_t recvfrom(int sockfd, void *buf, size_t len, int flags,
                            struct sockaddr *src_addr, socklen_t *addrlen);
    
           ssize_t sendmsg(int sockfd, const struct msghdr *msg, int flags);
           ssize_t recvmsg(int sockfd, struct msghdr *msg, int flags);

           read函数是负责从fd中读取内容.当读成功时,read返回实际所读的字节数,如果返回的值是0表示已经读到文件的结束了,小于0表示出现了错误。如果错误为EINTR说明读是由中断引起的,如果是ECONNREST表示网络连接出了问题。

            write函数将buf中的nbytes字节内容写入文件描述符fd.成功时返回写的字节数。失败时返回-1,并设置errno变量。 在网络程序中,当我们向套接字文件描述符写时有俩种可能。1)write的返回值大于0,表示写了部分或者是全部的数据。2)返回的值小于0,此时出现了错误。我们要根据错误类型来处理。如果错误为EINTR表示在写的时候出现了中断错误。如果为EPIPE表示网络连接出现了问题(对方已经关闭了连接)。

           其它的我就不一一介绍这几对I/O函数了,具体参见man文档或者baidu、Google。


    3.6、close()函数

           在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。

    #include <unistd.h>
    int close(int fd);

            close一个TCP socket的缺省行为时把该socket标记为以关闭,然后立即返回到调用进程。该描述字不能再由调用进程使用,也就是说不能再作为read或write的第一个参数。

           注意:close操作只是使相应socket描述字的引用计数-1,只有当引用计数为0的时候,才会触发TCP客户端向服务器发送终止连接请求。


    4、socket中TCP的三次握手建立连接详解

           我们知道tcp建立连接要进行“三次握手”,即交换三个分组。大致流程如下:

    • 客户端向服务器发送一个SYN J
    • 服务器向客户端响应一个SYN K,并对SYN J进行确认ACK J+1
    • 客户端再想服务器发一个确认ACK K+1

           只有就完了三次握手,但是这个三次握手发生在socket的那几个函数中呢?请看下图:

    image

    图1、socket中发送的TCP三次握手

           从图中可以看出,当客户端调用connect时,触发了连接请求,向服务器发送了SYN J包,这时connect进入阻塞状态;服务器监听到连接请求,即收到SYN J包,调用accept函数接收请求向客户端发送SYN K ,ACK J+1,这时accept进入阻塞状态;客户端收到服务器的SYN K ,ACK J+1之后,这时connect返回,并对SYN K进行确认;服务器收到ACK K+1时,accept返回,至此三次握手完毕,连接建立。

    总结:客户端的connect在三次握手的第二个次返回,而服务器端的accept在三次握手的第三次返回。


    5、socket中TCP的四次握手释放连接详解

           上面介绍了socket中TCP的三次握手建立过程,及其涉及的socket函数。现在我们介绍socket中的四次握手释放连接的过程,请看下图:

    image

    图2、socket中发送的TCP四次握手

    图示过程如下:

    • 某个应用进程首先调用close主动关闭连接,这时TCP发送一个FIN M;
    • 另一端接收到FIN M之后,执行被动关闭,对这个FIN进行确认。它的接收也作为文件结束符传递给应用进程,因为FIN的接收意味着应用进程在相应的连接上再也接收不到额外数据;
    • 一段时间之后,接收到文件结束符的应用进程调用close关闭它的socket。这导致它的TCP也发送一个FIN N;
    • 接收到这个FIN的源发送端TCP对它进行确认。

           这样每个方向上都有一个FIN和ACK。


    展开全文
  • UNIX_SOCKET 进程间通信

    千次阅读 2017-03-30 10:30:18
    使用socket实现进程间通信:(UNIX domain中面向连接通信)  使用套接字除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信。 man unix内容如下: NAME( ...
  • 这会是系列文章,讲解的内容也很简单,文章的目的是让自己的知识固话和文档化,以备自己不时的复习,同时也希望能够给予初学者一些帮助。 前面的文章系列文章有介绍了 linux 下常见的 IPC 机制,如管道、消息...
  • Unix系统下进程间通信方式及比较

    千次阅读 2014-08-27 14:35:00
    本文转载自: 进程间的通信方式:  1....  管道可用于具有亲缘关系... 信号是在软件层次上中断机制的种模拟,它是比较复杂的通信方式,用于通知进程有某事件发生,个进程收到个信号与处理器收到个中断
  • Unix编程】进程间通信(IPC)

    千次阅读 2015-04-21 20:52:06
    Linux进程间通信(IPC) 进程间通信(IPC,InterProcess Communication)是指在不同进程之间传播或交换信息。IPC的方式通常有管道(包括无名管道和命名管道)、消息队列、信号量、共享存储、Socket、Streams等。...
  • UNIX通信机制

    千次阅读 2011-04-20 10:54:00
    对Unix发展做出重大贡献的两大主力AT&T的贝尔实验室及BSD(加州大学伯克利分校的伯克利软件发布中心)在进程间通信方面的侧重点有所不同。前者对Unix早期的进程间通信手段进行了系统的改进和扩充,形成了“system ...
  • Linux高级进程间通信UNIX域套接字

    千次阅读 2014-08-14 18:51:10
    UNIX域套接字用于在同台机器上运行的进程间的通信。虽然因特网域套接字可用于同一目的,但UNIX域套接字的效率更高。UNIX域套接字仅仅复制数据,它们并不执行协议处理,不需要添加和删除网络报头,无需计算检验和,...
  • unix进程通信方式总结(下)

    千次阅读 2016-08-10 19:08:18
    在前两篇博客http://blog.csdn.net/caoyan_12727/article/details/52049417和http://blog.csdn.net/caoyan_12727/article/details/52126405中进程间的管道(pipe),命名管道(fifo),消息队列,信号量,信号,共享...
  • Unix域socket(总结)

    千次阅读 2017-07-23 09:58:46
    Unix域协议是在单个主机上执行客户/服务器通信种方法,用在本地进程间的通信,在不同进程之间传递套接字。 1)Unix域套接字不需要打包/拆包,计算校验和维护序号与应答,只是将应用层数据从个进程拷贝到另...
  • ubuntu实现unix domain socket通信

    千次阅读 2016-09-10 21:59:02
    socket API原本是为网络通讯设计的,但后来在socket的框架上发展出种IPC机制,就是UNIX Domain Socket。虽然网络socket也可用于同一台主机的进程间通讯(通过loopback地址127.0.0.1),但是UNIX Domain Socket用于...
  • 个进程想要操作共享数据,个进程共享数据 通知事 个进程需要向另个或组进程发送消息,通知它(它们)发生了某种事件(如进程终止时要通知父进程)。 资源共享 个进程之间共享同样的资源。为了作到这...
  • Unix域套接字

    千次阅读 2019-05-14 16:19:32
    Unxi域套接字并不是个实际的协议族,而是在单个主机上执行客户/服务器通信种方法,所用API就是在不同主机上执行客户/服务器通信所用的API。可以视为IPC方法之UNix域提供两类套接字:字节流套接字(类似...
  • windows和unix系统区别

    千次阅读 2013-10-19 23:10:50
    所以对多用户的支持windows就不及UNIX. 第三:UNIX采用的是用户进程与系统进程在内存中是分开的,用户进程的异常结束不会引起系统进程出现异常。 第四:UNIX采用的是对用户的授权到文件级,第个目录及文件都有...
  • 使用套接字除了可以实现网络间不同主机间的通信外,还可以实现同一主机的不同进程间的通信,且建立的通信是双向的通信。man unix内容如下:NAME( 名称) unix, PF_UNIX, AF_UNIX, PF_LOCAL, AF_LOCAL ? 用于本地...
  • BSD UNIX历史以及设计原则

    千次阅读 2016-01-25 17:34:45
    本博客是对一个BSD Unix历史的简介的翻译,原文是《操作系统概念》书附录A的部分 A.1 Unix历史 第Unix由贝尔实验室研究组的Ken Thompson于1969年开发,其目的是为了使用款闲置的电脑PDP-7.不久Dennis ...
  • Linux网络编程——Unix本地套接字

    千次阅读 2017-10-26 12:53:44
     发现很多人不知道或者不太了解 Unix 本地套接字这个概念,这也难怪,socket API 原本就是为台主机之间网络通信设计的,并且这种网络 socket 同样支持单台主机上的进程间通信,当然这样做的话,仍然需要 IP 地址...
  • AF_INET域与AF_UNIX域socket通信原理对比

    万次阅读 多人点赞 2014-11-08 18:49:36
    1. AF_INET域socket通信过程 典型的TCP/IP四层模型的通信过程。 发送方、接收方依赖IP:Port来标识,即将本地的socket绑定到对应的IP端口上,发送数据时,指定对方的IP端口,经过Internet,可以根据...
1 2 3 4 5 ... 20
收藏数 120,949
精华内容 48,379
热门标签
关键字:

unix一对多通信