【一】SYSTEM-V IPC概述:
(1) 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 共享内存
原理:
(1).共享内存概述
1.管道、 FIFO 和消息队列,任意两个进程之间想要交换信息,都必须通过内核,
内核在其中发挥了中转站的作用:
缺点: 一个通信周期内,上述过程至少牵扯到两次内存拷贝和两次系统调用
2.共享内存: 内核搭台, 进程唱戏。
步骤:1.内核负责构建出一片内存区域
2.两个或多个进程可以将这块内存区域映射到自己的虚拟地址空间
3.从此之后内核不再参与双方通信,进程之间使用共享内存通信
(当进程使用共享内存时,可能会发生缺页,引发缺页中断,这种情况下,内核还是会参与进来的。)
4.共享内存所有进程间通信方式中效率最高的一种.
5.进程可以像操作普通进程的地址空间一样操作这块共享内存,一个进程可以将信息写入这片内存区域,
而另一个进程也可以看到共享内存里面的信息,从而达到通信的目的
6.注意:
要防止多个进程同时操作同一块共享内存(使用信号量(生产者-消费者模型)搭配使用,来控制共享内存资源的互斥访问)
(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);
}