【一】SYSTEM-V IPC概述:
(1) System V IPC 编程接口
    第10章 进程间的通信: System V IPC

(2)SystemV IPC对象的删除
    1.消息队列: 立即删除消息队列        // 内核没有对使用消息队列的进程进行计数
    2.信号量:   立即删除信号量集        // 内核没有对使用信号量集的进程进行计数
    3.共享内存:    如果共享内存的引用计数等于 0,则可以立即删除共享内存。
                如果有多个进程在使用这段共享内存, 等所有进程都和这段共享内存分离(解除映射)后, 才能真正删除这段共享内存

(3)SystemV IPC对象的非文件属性
    1.System V IPC 对象在文件系统中没有实体文件与之关联。
    2.在shell中无法用 ls 查看存在的IPC对象,无法用 rm 将其删除。也无法用 chmod 来修改它们的访问权限
    3.Linux 提供了 ipcs、 ipcrm和ipcmk等命令来操作这些对象。
    4.System V IPC 对象不是文件描述符,所以无法使用基于文件描述符的多路转接I/O技术(select、poll和epoll等)。

(4)SystemV IPC对象的内核持久性
    1.哪怕创建 System V IPC 对象的进程已经退出,哪怕有一段时间没有任何进程打开该 IPC 对象,只要不执行
        删除操作或系统重启,后面启动的进程依然可以使用之前创建的 System V IPC 对象来通信。
(5)    标识符ID和IPC对象的作用域
    1.每种 SystemV IPC都有一个相关的get调用,该函数返回一个整型标识符ID, System V IPC 后续的函数操作都要作用在该标识符ID上。
    2.System V IPC对象的作用范围是整个操作系统,对于任何进程,无论是否存在亲缘关系,只要有相应的权限,都可以通过操作 System V IPC 对象来达到通信的目的。
------------------------------------------------------------------------------------------------------------------------------------------------------------    

【二】SystemV 共享内存
原理:
第10章 进程间的通信: System V IPC

(1).共享内存概述
    1.管道、 FIFO 和消息队列,任意两个进程之间想要交换信息,都必须通过内核,
      内核在其中发挥了中转站的作用:
第10章 进程间的通信: System V IPC

            缺点: 一个通信周期内,上述过程至少牵扯到两次内存拷贝和两次系统调用    

    2.共享内存: 内核搭台, 进程唱戏。
        步骤:1.内核负责构建出一片内存区域
              2.两个或多个进程可以将这块内存区域映射到自己的虚拟地址空间
              3.从此之后内核不再参与双方通信,进程之间使用共享内存通信
                (当进程使用共享内存时,可能会发生缺页,引发缺页中断,这种情况下,内核还是会参与进来的。)
              4.共享内存所有进程间通信方式中效率最高的一种.
              5.进程可以像操作普通进程的地址空间一样操作这块共享内存,一个进程可以将信息写入这片内存区域,
                 而另一个进程也可以看到共享内存里面的信息,从而达到通信的目的
              6.注意: 
                要防止多个进程同时操作同一块共享内存(使用信号量(生产者-消费者模型)搭配使用,来控制共享内存资源的互斥访问)
  第10章 进程间的通信: System V IPC

(2)创建或打开共享内存        
    int shmget(key_t key, size_t size, int shmflg);
        size: 必须是正整数,实际 size 会被向上取整为页面大小的整数倍。
        shmflg:支持 IPC_CREAT 和 IPC_EXCL 标志位 
        单个共享内存段的最大字节数为 SHMMAX //cat /proc/sys/kernel/shmmax

(3)使用共享内存: 将共享内存映射到进程的虚拟地址空间
    void *shmat(int shmid, const void *shmaddr, int shmflg);
        通常会将第二个参数设置为 NULL,表示由内核分配共享内存区域
        shmid: 为shmget返回的共享内存标识符ID
        shmflg: 如果进程仅仅是读取共享内存段的内容,并不修改,则可以指定 SHM_RDONLY 标志位。
    // SHM_KEY所在的共享内存不存在时创建他, 若存在返回错误
    第一个进程: shmid = shmget(SHM_KEY, SHM_SIZE, IPC_CREAT | IPC_EXCL | 0777);
    // 打开SHM_KEY所在的共享内存
    第二个进程: shmid = shmget(SHM_KEY, SHM_SIZE, 0777);

(4)    分离共享内存
    int shmdt(const void *shmaddr);
    1.shmdt 函数仅仅是使进程和共享内存脱离关系,并未删除共享内存
    2.shmdt 函数的作用是将共享内存的引用计数减 1 
    3.只有共享内存的引用计数为 0 时,调用 shmctl 函数的 IPC_RMID 命令才会真正地删除共享内存。

(5)控制共享内存:
    int shmctl(int shmid, int cmd, struct shmid_ds *buf);
        IPC_STAT: 获取 shmid 对应的共享内存的信息(struct shmid_ds信息)
        IPC_SET:  只能修改 shm_perm 中的 uid 、 gid 及 mode 。    
        IPC_RMID: 如果共享内存的引用计数 shm_nattch 等于 0 ,则可以立即删除共享内存。但是如果仍然存在进程
                  attach 该共享内存,则并不执行真正的删除操作,而仅仅是设置 SHM_DEST 标记。待所有进程都执行过
                  分离操作之后,再执行真正的删除操作。
                  //,共享内存处于 SHM_DEST 状态的情况下,依然允许新的进程调用 shmat 函数来 attach该共享内存。
        ...........

(6)key的获取方法(**)
    方法1: (1-5000)内的任意整数
    方法2: key_t ftok(const char *pathname, int proj_id); // 任意合法路径,任意整数

------------------------------------------------------------------------------------------------------------------------------------------------------------    
【三】.消息队列(SYSTEM-V IPC)
(1)创建或打开一个消息队列
    int msgget(key_t key, int msgflg);

(2)发送消息
    int msgsnd(int msqid, const void *msgp, size_t msgsz, int msgflg);
     msqid: 是由 msgget 返回的标识符 ID 。
     msgp:  指向用户定义的缓冲区。它的第一个成员必须是一个指定消息类型的 long 型,后面跟着消息文本的内容
            struct msgbuf {
                long mtype;     /* 消息类型,必须大于0 */
                char mtext[1];  /* 消息体,不一定是字符数组,可以是任意结构*/
            };
     msgsz:消息的大小
     msgflg:IPC_NOWAIT 表示执行一个无阻塞的发送操作。
                (1)无IPC_NOWAIT标志位:
                    如果消息队列满了,msgsnd函数会陷入阻塞,直到队列有足够的空间来存放这条消息为止。
                (2)有IPC_NOWAIT标志位:
                    如果消息队列满了,msgsnd函数就不会陷入阻塞了,而是立刻返回失败,并置 errno 为 EAGAIN 。
     返回值: 成功返回消息队列ID(非负)

(3)    接收消息
    ssize_t msgrcv(int msqid, void *msgp, size_t msgsz,long msgtyp,int msgflg);     
     msgtyp:
        msgtyp == 0: 最先进入消息队列的消息被取出。
        msgtyp < 0:  mtype 的值越低,其优先级越高,越早被取出。
        msgtyp > 0:,会将消息队列中第一条 mtype 值等于 msgtyp 的消息取出。通过指定不同的 msgtyp ,多个进程可以在同一个消息队列中
        挑选各自感兴趣的消息。一种常见的场景是各个进程提取和自己进程 ID 匹配的消息。    
     msgflg:
     ·IPC_NOWAIT :如果消息队列中不存在满足 msgtyp 要求的消息,默认情况是阻塞等待,但是一旦设置了 IPC_NOWAIT 标志位,则立即返回失败,并且设置 errno 为 ENOMSG 。
     ·MSG_EXCEPT :这个标志位是 Linux 特有的,只有当 msgtyp 大于 0 时才有意义,含义是选择 mtype ! =msgtyp 的第一条消息
     ·MSG_NOERROR :在消息体变长的情况下,此时会将消息体截断并返回。

(4)控制消息队列
    int msgctl(int msqid, int cmd, struct msqid_ds *buf);
        IPC_RMID:     删除消息队列(立即生效, 因为内核没有为使用消息队列的进程维护引用计数)
            所有阻塞的msgsnd函数和msgrcv函数会被唤醒,并返回EIDRM 错误。
        IPC_SET:    设置消息队列
        IPC_STAT:    获取消息队列的状态

(5)优缺点
    1.当消息队列中有消息到来时,无法主动通知到某进程, 必须靠进程主动去接收消息。
    2.消息队列的读取进程,只能用阻塞或轮询的方式。这就意味着一个进程或线程不得不无所事事,盯在该消息队列上。
    3.如果 System V消息队列是文件,能支持 select 、 poll 和 epoll 等I/O多路转接函数,一个进程就能同时监控多个文件(或者多个消息队列),提
      供更灵活的编程模式。可惜的是, System V 消息队列并非文件,不支持I/O多路转接函数。
    4.用一个消息队列可进行多个进程间的通信(通过进程接收的消息类型不同) 

(6)示例代码: 用消息队列编写了3个进程之间任意聊天的小程序
------------------------------------------------------------------------------------------------------------------------------------------------------------    

【四】.SYSTEM-V IPC信号量(进程间同步互斥)

(1)概述
    1.消息队列的作用是进程之间传递消息。
    2.信号量的作用是为了同步多个进程的操作
    3.内核会负责维护信号量的值,并确保其值不小于 0 。
    4.信号量上支持的操作
        · 将信号量的值设置成一个绝对值。
        · 在信号量当前值的基础上加上一个数量。
        · 在信号量当前值的基础上减去一个数量。
        · 等待信号量的值等于 0 。
    5.二值信号量
        1.它只有两种合法值: 0 和 1 ,对应一个可用的资源。
        2.若当前有资源可用,则与之对应的二值信号量的值为 1 ;
        3.若资源已被占用,则与之对应的二值信号量的值为 0 。
        4.当进程申请资源时,如果当前信号量的那么进程会陷入阻塞,直到有其他进程释放资源,将信号量的值加 1 才能被唤醒。
    6.互斥量( mutex )是用来保护临界区的,所谓临界区,是指同一时间只能容许一个进程进入。
      而信号量( semaphore )是用来管理资源的,资源的个数不一定是 1 ,可能同时存在多个一模一样的资源,
        因此容许多个进程同时使用资源。
    7.信号量是互斥量的一个扩展,由于资源数目增多,增强了并行度。但是这仅仅是一个方面。更重要的区别是,互斥量和信号量解决的问题是不同的。
    8.互斥量的关键在于互斥、排它,同一时间只允许一个线程访问临界区。决定了解铃还须系铃人,即加锁进程必然也是解锁进程, 
    9.而信号量的关键在于资源的多少和有无。申请资源的进程不一定要释放资源,信号量同样可以用于生产者 - 消费者的场景。在这种场景下,
      生产者进程只负责增加信号量的值,而消费者进程只负责减少信号量的值。彼此之间通过信号量的值来同步。
    10.和二值信号量相比, System V 信号量在两个维度上都做了扩展。
        第一,资源的数目可以是多个。资源个数超过 1 个的信号量称为计数信号量
        第二,允许同时管理多种资源,由多个计数信号量组成的一个集合称为计数信号量集,每个计数信号量管理一种资源。
    11.    fork前创建的信号量,父子进程的信号量是实时自动同步的

(2)创建或打开信号量
    int semget(key_t key, int nsems, int semflg);
        第二个参数 nsems 表示信号量集中信号量的个数。换句话说,就是要控制几种资源。大部分情况下只控制一种。
        semflg 支持多种标志位。目前支持 IPC_CREAT 和 IPC_EXCL 标志位,其含义不再赘述。

(3)操作信号量
    int semop(int semid, struct sembuf *sops, unsigned nsops);
    第二个参数是 sembuf 类型的指针, sem_num 解决的是操作哪个信号量的问题    
    第三个参数是表示要同时操作几个信号量
    通常的方法:
    1.抢占资源型
        同一个资源互斥访问:         
            [资源]
            setctl    1            // 初始化为1
        p1            p2
        semop-1        semop-1     // 使用资源前,必须先占有资源
        使用临界资源资源
        sleep(1)    sleep(1)    // 将当前进程挂起1秒, 给其他进程抢占资源的机会
        sem+1        sem+1        // 释放资源 
    2.生产者消费者类型                //(共享内存和信号量的搭配使用就是)
            [资源]
            setctl    0            // 初始化为0
        生产者        消费者
        semop+1     semop-1        // 二者完全独立, 当资源消耗完后, 消费者陷入阻塞
    
(4)控制信号量    
    int semctl(int semid, int semnum, int cmd,/* union semun arg*/);

(5)通常的封装
    

union semun {
        int              val;    /* Value for SETVAL */
        struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
        unsigned short  *array;  /* Array for GETALL, SETALL */
        struct seminfo  *__buf;  /* Buffer for IPC_INFO     (Linux-specific) */
    };
    // 获取信号量集中指定信号量的值
    int sema_getval(int semid, int index)
    {    union semun ignore_arg;
        return semctl(semid, index, GETVAL, ignore_arg);
    }
    // 设置信号量集中指定信号量为value
    int sema_setval(int semid, int index, int val)
    {    union semun arg;
        arg.val = val;
        return semctl(semid, index, SETVAL, arg);
    }
    // 立即删除信号量集
    int sema_rmid(int semid)
    {    union semun ignore_arg;
        int ignore_index;
        return semctl(semid, ignore_index, IPC_RMID, ignore_arg);
    }
    /* 操作信号量 */ 
    // 申请资源(操作一个信号量)
    int semaphore_wait (int semid, int index)
    {
        struct sembuf operations[1];
        operations[0].sem_num = index;            // 操作一组信号量中的哪一个信号量
        operations[0].sem_op = -1;                // 信号量0的值 + 1
        operations[0].sem_flg |= SEM_UNDO;        // 进程退出后恢复信号量(成对出现)
        return semop(semid, operations, 1);    // 信号量集合操作
    }
    // 释放资源(操作一个信号量)
    int semaphore_post(int semid, int index)
    {
        struct sembuf operations[1];
        operations[0].sem_num = index;            // 操作一组信号量中的哪一个信号量
        operations[0].sem_op = 1;                // 信号量0的值 - 1
        operations[0].sem_flg |= SEM_UNDO;        // 进程退出后恢复信号量(成对出现)
        return semop(semid, operations, 1);
    }

 

相关文章: