共享内存和信号量
共享内存
🌔🌔🌔🌔🌔
- 共享内存就是使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。
- 共享内存在进程空间的映射:
使用共享内存通信的一般步骤
1、创建或者打开共享内存
2、进程A连接(映射)共享内存,写入数据
3、进程A断开
4、进程B连接(映射)共享内存,读取数据
5、进程B断开
6、释放共享内存
- 示意图:
创建共享内存
-
shmget函数:
#include <sys/ipc.h> #include <sys/shm.h> int shmget(key_t key, size_t size,int shmflg);
- 功能:创建或打开一块共享内存区。
- 参数:
- key:进程间通信键值,ftok() 的返回值。
- size:该共享存储段的长度(字节)。
- shmflg:标识函数的行为及共享内存的权限,其取值如下:
- IPC_CREAT:如果不存在就创建
- IPC_EXCL: 如果已经存在则返回失败
- 位或权限位:共享内存位或权限位后可以设置共享内存的访问权限
- 返回值: 成功返回共享内存标识符;失败返回-1。
共享内存的映射和解除映射
-
shmat函数:
#include <sys/types.h> #include <sys/shm.h> void *shmat(int shmid, const void *shmaddr, int shmflg);
-
功能: 将一个共享内存段映射到调用进程的数据段中
-
参数:
- shmid:共享内存标识符,shmget() 的返回值。
- shmaddr:共享内存映射地址(若为 NULL 则由系统自动指定),推荐使用 NULL。
- shmflg:共享内存段的访问权限和映射条件( 通常为 0 ),具体取值如下:
- 0:共享内存具有可读可写权限。
- SHM_RDONLY:只读。
- SHM_RND:(shmaddr 非空时才有效)
-
返回值: 成功返回共享内存段映射地址( 相当于这个指针就指向此共享内存 ) ;失败返回-1
-
-
shmdt函数:
#include <sys/types.h> #include <sys/shm.h> int shmdt(const void *shmaddr);
-
功能: 将共享内存和当前进程分离
-
参数:
- shmaddr:共享内存映射地址。
-
返回值: 成功0 ;失败-1
-
共享内存操作函数
-
shmct函数:
#include <sys/ipc.h> #include <sys/shm.h> int shmctl(int shmid, int cmd, struct shmid_ds *buf);
- 功能: 共享内存属性的控制。
- 参数:
-
shmid:共享内存标识符。
-
cmd:函数功能的控制,其取值如下:
- IPC_RMID:删除
- IPC_SET:设置 shmid_ds 参数,相当于把共享内存原来的属性值替换为 buf 里的属性值。
- IPC_STAT:保存 shmid_ds 参数,把共享内存原来的属性值备份到 buf 里。
- SHM_LOCK:锁定共享内存段( 超级用户 )。
- SHM_UNLOCK:解锁共享内存段。
- SHM_LOCK 用于锁定内存,禁止内存交换。并不代表共享内存被锁定后禁止其它进程访问。
-
buf:shmid_ds 数据类型的地址,用来存放或修改共享内存的属性。
-
-
返回值: 成功0 ;失败-1
示例代码
-
本例程未使用信号量,写端停留5秒等待读端读取信息,配合信号量使用的代码见文末
-
写进程shm_w.c
#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <string.h> #include <stdlib.h> int main() { int shmid; //共享内存标识符 char *shmaddr; key_t key; key = ftok(".",1); //获取键值 shmid = shmget(key,1024*4,IPC_CREAT|0666); //打开或者创建共享内存 if(shmid == -1){ printf("shmget NO OK\n"); exit(-1); } shmaddr = shmat(shmid,0,0); //共享内存连接到当前进程的地址空间 printf("shmat ok\n"); strcpy(shmaddr,"hello world"); //向内存中写入数据 sleep(5); //睡眠5秒,等待内存数据被读走 shmdt(shmaddr); //断开进程和内存的连接 shmctl(shmid,IPC_RMID,0); //删除共享内存段 printf("quit\n"); return 0; }
-
读进程shm_r.c
#include <sys/ipc.h> #include <sys/shm.h> #include <stdio.h> #include <string.h> #include <stdlib.h> int main() { int shmid; char *shmaddr; key_t key; key = ftok(".",1); //获取键值 shmid = shmget(key,1024*4,0); //打开创建的共享内存,获取内存ID, if(shmid == -1){ printf("shmget no ok\n"); exit(-1); } shmaddr = shmat(shmid,0,0); //共享内存连接到当前进程的地址空间 printf("shmat ok\n"); //表示连接成功 printf("data : %s",shmaddr); //将内存地址中的数据读出,打印 shmdt(shmaddr); //断开内存和当前进程的连接 printf("quit\n"); return 0; }
信号量
🌔🌔🌔🌔🌔
多任务编程中互斥和同步的概念
- 互斥:是指散步在不同任务之间的若干程序片断,当某个任务运行其中一个程序片段时,其它任务就不能运行它们之中的任一程序片段,只能等到该任务运行完这个程序片段后才可以运行。最基本的场景就是:一个公共资源同一时刻只能被一个进程或线程使用,多个进程或线程不能同时使用公共资源。
- 同步:是指散步在不同任务之间的若干程序片断,它们的运行必须严格按照规定的某种先后次序来运行,这种先后次序依赖于要完成的特定的任务。最基本的场景就是:两个或两个以上的进程或线程在运行过程中协同步调,按预定的先后次序运行。比如 A 任务的运行依赖于 B 任务产生的数据。
信号量(Semaphore)
-
信号量广泛用于进程或线程间的同步和互斥,信号量本质上是一个非负的整数计数器,它被用来控制对公共资源(比如上文说的共享内存)的访问。
-
信号量可以用来保证两个或多个关键代码段不被并发调用。
-
在实际应用中两个进程间通信可能会使用多个信号量,因此 System V 的信号量以集合的概念来管理。
-
信号量集合数据结构:struct semid_ds,此数据结构中定义了整个信号量集的基本属性。
/* Obsolete, used only for backwards compatibility and libc5 compiles */ struct semid_ds { struct ipc_perm sem_perm; /* permissions .. see ipc.h */ __kernel_time_t sem_otime; /* last semop time */ __kernel_time_t sem_ctime; /* last change time */ struct sem *sem_base; /* ptr to first semaphore in array */ struct sem_queue *sem_pending; /* pending operations to be processed */ struct sem_queue **sem_pending_last; /* last pending operation */ struct sem_undo *undo; /* undo requests on this array */ unsigned short sem_nsems; /* no. of semaphores in array */ };
-
信号量数据结构:struct sem,此数据结构中定义了信号量的基本属性。
/* One semaphore structure for each semaphore in the system. */ struct sem { int semval; /* current value *信号量的值*/ int sempid; /* pid of last operation *最后一个操作信号量的进程号*/ struct list_head sem_pending; /* pending single-sop operations */ };
信号量的操作
-
编程时可根据操作信号量值的结果判断是否对公共资源具有访问的权限,当信号量值大于 0 时,则可以访问,否则将阻塞。
-
PV 原语是对信号量的操作,一次 P 操作使信号量减1,一次 V 操作使信号量加1。(P表示通过,V表示释放)
- 🔒P 操作:将sem减 1,相减后,如果 sem<0 ,则进程/线程进入阻塞等待,否则继续,表明 P 操作可能会阻塞
- 🔑V 操作:将 sem 加 1,相加后,如果 sem>0 ,唤醒一个等待中的进程/线程,表明 V 操作不会阻塞
- 只有sem大于0时P才不会对进程阻塞
- P 操作是用在进入临界区之前,V 操作是用在离开临界区之后,这两个操作一般是成对出现的。
-
信号量用于互斥:
-
信号量用于同步:
相关函数
-
创建信号量数组
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semget(key_t key, int nsems, int semflg);
-
功能: 创建或打开一个信号量集合,该集合中可以包含多个信号量。
-
参数:
- key:进程间通信键值,通过调用 ftok() 函数得到的键值
- nsems:创建的信号量的个数。如果只是访问而不创建则可以指定该参数为 0,一旦创建了该信号量, 就不能更改其信号量个数,只要不删除该信号量,重新调用该函数创建该键值的信号量,该函数只是返回以前创建的值,不会重新创建。
- semflg:标识函数的行为及信号量的权限,其取值如下:
- IPC_CREAT:创建信号量。
- IPC_EXCL:检测信号量是否存在。
- 位或权限位:信号量位或权限位后可以设置信号量的访问权限,格式和 open 函数的 mode_ t一样(open() 的使用请点此链接),但可执行权限未使用。
-
返回值: 成功返回信号量集标识符;失败返回 -1
-
-
semop函数对信号量组进行操作
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semop(int semid, struct sembuf *sops, size_t nsops); struct sembuf{ unsigned short sem_num; /*信号量的序号*/ short sem_op; /*信号量的操作值*/ short sem_flg; /*信号量的操作标识*/ };
-
功能: 操作信号量,主要进行信号量加减操作。
-
参数:
- semid:信号量集标识符。
- sops:操作信号量的结构体(struct sembuf)数组的首地址( 结构体定义在 sys/sem.h ),此结构体中的数据表明了对信号量进行的操作。
-
结构体成员使用说明如下:
-
sem_num:信号量集中信号量的序号
-
sem_op 取值如下:
- sem_op > 0:信号量的值在原来的基础上加上此值。
- sem_op < 0:如果信号量的值小于 semop 的绝对值,则挂起操作进程。
- sem_op = 0:对信号量的值进行是否为 0 测试。若为 0 则函数立即返回,若不为 0 则阻塞调用进程
- 如果信号量的值大于等于 semop 的绝对值, 则信号量的值在原来的基础上减去 semop 的绝对值。
-
sem_flag 取值如下:
- IPC_NOWAIT:在对信号量的操作不能执行的情况下使函数立即返回。
- SEM_UNDO:当进程退出后,该进程对信号量进行的操作将被撤销。
-
- nsops:操作信号量的结构体数组中元素的个数。
-
返回值: 成功0 ;失败-1
-
-
semctl函数控制信号量的相关信息
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> int semctl(int semid, int semnum, int cmd, ...); union semun{ int val; /*信号量的值*/ struct semid_ds *buf; /*信号量集合信息*/ unsigned short *array;/*信号量值的数组*/ struct seminfo *__buf;/*信号量限制信息*/ };
-
功能: 对信号量集合以及集合中的信号量进行操作。
-
参数:
- semid:信号量集标识符
- semnum:集合中信号量的序号,指定对哪个信号量操作, 只对几个特殊的 cmd 操作有意义
- cmd:信号量控制类型。semctl() 函数可能有3个参数,也可能有4个参数,参数的个数由 cmd 决定。第4个参数为联合体。
- cmd 的取值如下:
-
GETVAL:获取信号量的值。此时函数有3个参数。semctl() 函数的返回值即为信号量的值
-
SETVAL:设置信号量的值。此时函数有4个参数。第4个参数为联合体中的val,其值为信号量的值。
-
IPC_STAT:获取信号量集合的信息。此时函数有4个参数。第4个参数为联合体中的__buf
-
IPC_SET:设置信号量集合的信息。此时函数有4个参数。第4个参数为联合体中的__buf
-
IPC_RMID:删除信号量集。此时函数有3个参数,第2个参数semnum不起作用
-
GETALL:获取所有信号量的值。此时函数有4个参数,第2个参数semnum不起作用。第4个参数为联合体中的array,其值为用来存放所有信号量值的数组的首地址。
-
SETALL:设置所有信号量的值 ,参数说明同上
-
IPC_INFO:获取信号量集合的限制信息。此时函数有4个参数,第2个参数semnum不起作用。
-
GETPID:获取信号的进程号,即最后操作信号量的进程。此时函数有3个参数。 semctl() 函数的返回值即为信号的进程号。
-
GETNCNT:获取等待信号的值递增的进程数。此时函数有3个参数。semctl() 函数的返回值即为进程数。
-
GETZCNT:获取等待信号的值递减的进程数。此时函数有3个参数。semctl() 函数的返回值即为进程数。
-
-
返回值: 成功0 ;失败-1
-
函数使用示例
-
下例将sem设为0,父进程使用P操作会等待子进程V操作释放
#include <sys/types.h> #include <sys/ipc.h> #include <sys/sem.h> #include <stdio.h> #include <unistd.h> 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) */ }; void pGetKey(int semid) { struct sembuf set; set.sem_num = 0; set.sem_op = -1; set.sem_flg = SEM_UNDO; semop(semid, &set,1); printf("get key\n"); } void vPutKey(int semid) { struct sembuf set; set.sem_num = 0; set.sem_op = 1; set.sem_flg = SEM_UNDO; semop(semid, &set,1); printf("put key\n"); } int main() { key_t key = ftok(".",2); int semid = semget(key,1,IPC_CREAT|0666); union semun seminit; seminit.val = 0; semctl(semid,0,SETVAL,seminit); int pid = fork(); if(pid > 0){ pGetKey(semid); printf("this is father\n"); vPutKey(semid); semctl(semid,0,IPC_RMID); }else if(pid == 0){ printf("this is child\n"); sleep(5); vPutKey(semid); }else{ printf("fork error!\n"); } return 0; }
信号量配合共享内存使用
- 示意图:
-
参考信号量的同步模型,建立两个信号量,一个为0,另一个为1
-
写进程shm_w.c:
#include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <stdio.h> #include <string.h> #include <stdlib.h> 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) */ }; void P(int semid,unsigned short num) { struct sembuf set; set.sem_num = num; //信号量在数组里的序号 set.sem_op = -1; //信号量的操作值 set.sem_flg = SEM_UNDO; //信号量的操作标识 semop(semid, &set,1); printf("get key\n"); } void V(int semid,unsigned short num) { struct sembuf set; set.sem_num = num; set.sem_op = 1; set.sem_flg = SEM_UNDO; semop(semid, &set,1); printf("put key\n"); } int main() { key_t key1,key2; key1 = ftok(".",1); //获取键值 key2 = ftok(".",2); int shmid = shmget(key1,1024*4,IPC_CREAT|0666); //打开或者创建共享内存 int semid = semget(key2,2,IPC_CREAT|0666);//打开或者创建信号量组 union semun seminit; //信号量初始化 seminit.val = 1; //第一个信号量设置为1 semctl(semid,0,SETVAL,seminit); seminit.val = 0;//第二个信号量设置为0 semctl(semid,1,SETVAL,seminit); P(semid,0); //给第一个信号量上锁 char *shmaddr = shmat(shmid,0,0); //共享内存连接到当前进程的地址空间 printf("shmat ok\n"); strcpy(shmaddr,"hello world"); //向内存中写入数据 shmdt(shmaddr); //断开进程和内存的连接 V(semid,1); //释放第二个信号量 P(semid,0); //等待第一个信号量的释放 shmctl(shmid,IPC_RMID,0); //删除共享内存段 semctl(semid,0,IPC_RMID); //删除信号量组 printf("quit\n"); return 0; }
-
读进程shm_r.c:
#include <sys/ipc.h> #include <sys/shm.h> #include <sys/sem.h> #include <stdio.h> #include <string.h> #include <stdlib.h> 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) */ }; void P(int semid,unsigned short num) { struct sembuf set; set.sem_num = num; //信号量在数组里的序号 set.sem_op = -1; //信号量的操作值 set.sem_flg = SEM_UNDO; //信号量的操作标识 semop(semid, &set,1); printf("get key\n"); } void V(int semid,unsigned short num) { struct sembuf set; set.sem_num = num; set.sem_op = 1; set.sem_flg = SEM_UNDO; semop(semid, &set,1); printf("put key\n"); } int main() { key_t key1,key2; key1 = ftok(".",1); //获取键值 key2 = ftok(".",2); int shmid = shmget(key1,1024*4,IPC_CREAT|0666); //打开或者创建共享内存 int semid = semget(key2,1,IPC_CREAT|0666);//打开或者创建信号量组 P(semid,1); //等待第二个信号量释放 char *shmaddr = shmat(shmid,0,0); //共享内存连接到当前进程的地址空间 printf("shmat ok\n"); //表示连接成功 printf("data : %s\n",shmaddr); //将内存地址中的数据读出,打印 shmdt(shmaddr); //断开内存和当前进程的连接 V(semid,0); //释放第一个信号量 printf("quit\n"); return 0; }
-
因为信号量的初始化在写端,所以先执行写端
-
终端显示:
-
写端:
-
读端:
-