LV05-05-进程通信-06-信号量
本文主要是进程通信——信号量的相关笔记,若笔记中有错误或者不合适的地方,欢迎批评指正😃。
点击查看使用工具及版本
Windows | windows11 |
Ubuntu | Ubuntu16.04的64位版本 |
VMware® Workstation 16 Pro | 16.2.3 build-19376536 |
SecureCRT | Version 8.7.2 (x64 build 2214) - 正式版-2020年5月14日 |
开发板 | 正点原子 i.MX6ULL Linux阿尔法开发板 |
uboot | NXP官方提供的uboot,NXP提供的版本为uboot-imx-rel_imx_4.1.15_2.1.0_ga(使用的uboot版本为U-Boot 2016.03) |
linux内核 | linux-4.15(NXP官方提供) |
STM32开发板 | 正点原子战舰V3(STM32F103ZET6) |
点击查看本文参考资料
参考方向 | 参考原文 |
--- | --- |
点击查看相关文件下载
--- | --- |
一、信号量
其实前边我们在学习进程通信的时候,会不会有一个疑问,就是多个进程同时写一块区域的时候咋办?或者说那个进程在写,这个来读,读到的数据会不会就有问题呢?进程中没有像线程的互斥锁这些东西来保护共享的资源,但是我们可以使用信号量来达到相同的效果。
1. 信号量简介
信号量是一个计数器,与其它进程间通信方式不大相同,它主要用于控制多个进程间或一个进程内的多个线程间对共享资源的访问,相当于内存中的标志,进程可以根据它判定是否能够访问某些共享资源,同时,进程也可以修改该标志,除了用于共享资源的访问控制外,还可用于进程同步。
常被作为一种锁机制,防止某进程在访问资源时其它进程也访问该资源,因此,信号量主要作为进程间以及同一个进程内不同线程之间的同步手段,而不是用于缓存进程间通信的数据。
2. 信号量的操作
信号量代表某一类资源,其值表示系统中该资源的数量,只能通过三种操作来访问它:
(1)初始化
(2)
P
操作(申请资源)(3)
V
操作(释放资源)
【注意事项】P
操作是用在进入共享资源之前,V
操作是用在离开共享资源之后,这两个操作是必须成对出现的。
2.1 P 操作(申请资源)
P
操作其实就是将信号量减去 1
,相减后如果信号量< 0
,则表明资源已被占用,没有资源可用,进程需阻塞等待,进入等待队列;相减后如果信号量 >= 0
,则表明还有资源可使用,进程可正常继续执行。也就是
1 | 信号量的值 - 1; |
2.2 V 操作(释放资源)
V
操作会把信号量加上 1
,相加后如果信号量 <= 0
,则表明当前有阻塞中的进程,于是会从等待队列中取出一个进程唤醒运行;相加后如果信号量 > 0
,则表明当前没有阻塞中的进程。也就是
1 | 信号量的值 + 1; |
3. 互斥信号量
上边的已经了解了信号量的操作,那具体是怎样工作的呢?比如我们现在有两个进程:进程1
和进程2
,还有一个信号量sem
,我们将信号量sem
初始化为1
然后进程1
和进程2
都要访问共享内存。
进程 1
在访问共享内存前,先执行了 P
操作,信号量sem
的初始值为 1
,故在进程 1
执行 P
操作后信号量sem
变为 0
,表示共享内存可用,于是进程 1
就可以访问共享内存。
进程1
正在访问共享内存,进程 2
也想访问共享内存,执行了 P
操作,会使信号量sem
变为 -1
,这就意味着共享内存已被占用,因此进程 2
被阻塞。
当进程 1
访问完共享内存,才会执行 V
操作,使得信号量sem
恢复为 0
,接着就会唤醒阻塞中的线程 2
,使得进程 2
可以访问共享内存,最后完成共享内存的访问后,执行 V
操作,信号量sem
恢复到初始值 1
。
由此我们知道,信号初始化为 1
,就代表着是互斥信号量,它可以保证共享内存在任何时刻只有一个进程在访问,这就达到了保护了共享内存的目的。
4. 同步信号量
前边我们在学习线程的时候,有一个生产者与消费者的问题,生产者生产了产品后,消费者才能使用产品,进程之间是否也可以这样呢?我们假设有两个进程,分别为进程1
和进程2
,进程1
负责生产产品,进程2
负责使用产品,初始的信号量sem
我们初始化为0
。
如果进程 2
比进程 1
先执行,那么执行到 P
操作时,而信号量sem
初始值为 0
,所以信号量sem
会变为 -1
,表示进程 1
还没生产产品,于是进程 2
就阻塞等待;
当进程 1
生产完产品后,执行了 V
操作,就会使得信号量sem
变为 0
,于是就会唤醒阻塞在 P
操作的进程 2
;
进程 2
被唤醒后,意味着进程 1
已经生产了数据,于是进程 2
就可以正常使用产品啦了。
由此可知,信号初始化为 0
,就代表着是同步信号量,这样可以保证进程 1
应在进程 2
之前执行。
5. 信号量种类
一共有三种信号量,分别是POXIX
中的无名信号量和有名信号量,以及System V
信号量。
POSIX
中的无名信号量主要用于线程间的通信,保存在内存中,如果想要在进程间同步就必须把无名信号量放在进程间的共享内存中。
而在进程间的通信中同步用的通常是有名信号量,有名信号量一般保存在/dev/shm/
目录下,它就像文件一样存储在文件系统中。
System V
信号量是一个或多个计数信号量的集合,可同时操作集合中的多个信号量,并且申请多个资源时可以避免死锁。
二、POSIX 有名信号量
这部分是关于有名信号量使用的笔记,需要注意的是有名信号量是随内核持续的,所以如果我们不调用sem_unlink
来删除它,它将会一直存在,直到内核重启。另外,该信号量的使用需要借助于共享内存,打开或者创建之前需要进行以下步骤:
- (1)申请
key
; - (2)创建共享内存区域;
- (3)映射内存区域到进程的虚拟地址空间。
1. 创建或打开有名信号量
1.1 sem_open()
1.1.1 函数说明
在linux
下可以使用man 3 sem_open
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于打开或者创建一个有名信号量。
【函数参数】
name
:char *
类型,该参数表示信号量的名字(最好不要包含路径,可能会报no such file or diratory
)。
使用 man 7 sem_overview查看该参数详细的说明
1 | A named semaphore is identified by a name of the form /somename; that is, a null-terminated string of up to NAME_MAX-4 (i.e., 251) characters consisting of an initial slash, followed by one or more characters, none of which are slashes. Two processes can operate on the same named semaphore by passing the same name to sem_open(3). |
很明显说明,name
参数的构造是以 /
号开头,后面跟的字符串不能再有 /
号,长度小于NAME_MAX - 4
。所以最好不要在这里指定一个路径,创建的有名信号灯文件默认都保存在/dev/shm
路径下,也不需要我们来指定路径。
oflag
:int
类型,选择创建或者打开一个信号量,由于此函数会创建有名信号量文件,所以这里一般可以通过|
与表示文件存取权限的宏(如O_RDWR
)一起使用。 一般选择O_CREAT|O_RDWR
参数,表示以可读写的方式创建并初始化一个信号量,此时mode
和value
参数是被需要的。如果指定了O_CREAT
,并且给定名称的信号量已经存在,那么mode
和value
将被忽略。
点击查看 oflag 参数取值
我是查看了man
手册,大概就是这两点吧。
O_CREAT | 如果信号量不存在,就会创建信号量并初始化,函数第三个和第四个参数是必要的的。 |
O_CREAT|O_EXCL | 如果给定名称的信号量已经存在,则返回一个错误。 |
mode
:mode_t
类型,表示文件权限,常用参数为0666
。value
:unsigned int
类型,表示信号量的初始值。该初始不能超过SEM_VALUE_MAX
,这个常值必须低于为32767
。二值信号量的初始值通常为1
,计数信号量的初始值则往往大于1
。
【返回值】sem_t *
类型,成功返回新信号量的地址,失败返回SEM_FAILED
,并设置errno
表示错误。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】创建的信号量文件在/dev/shm
目录下。
1.1.2 使用实例
暂无。
2. 关闭有名信号量
2.1 sem_close()
2.1.1 函数说明
在linux
下可以使用man 3 sem_close
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于关闭一个信号量。
【函数参数】
sem
:sem_t *
类型,需要关闭的信号量的地址。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
暂无。
3. 删除有名信号量
3.1 sem_unlink()
3.1.1 函数说明
在linux
下可以使用man 3 sem_unlink
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于删除一个有名信号量。
【函数参数】
name
:char *
类型,表示需要删除的有名信号量的名称,若名称并非使用变量指定,可以直接将名称用" "
包裹即可。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】当我们一个进程运行完毕的时候,信号量是一直存在的,当我们下一次重新运行这个进程,由于信号量存在,这就可能会导致出现一些问题,所以我们一般会自定义一个信号处理函数,在信号处理函数中使用该删除函数删除有名信号量。
3.1.2 使用实例
暂无。
4. 有名信号量的P
操作
P
操作其实就是获取资源,如果信号量为0
,表示这时没有相应资源空闲,那么调用进程就将挂起,直到有空闲资源可以获取。
4.1 sem_wait()
4.1.1 函数说明
在linux
下可以使用man 3 sem_wait
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的P
操作,也就是获取资源。如果信号量的值大于0
,则信号量继续递减,函数立即返回;如果信号量当前的值是0
,那么调用就会阻塞,直到可以执行递减操作(即信号量的值升到0
以上),或者信号处理程序中断调用。
【函数参数】
sem
:sem_t *
类型,表示需要等待的信号量。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
4.1.2 使用实例
暂无。
4.2 sem_trywait()
4.2.1 函数说明
在linux
下可以使用man 3 sem_trywait
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的P
操作。sem_trywait()
与sem_wait()
相同,不同之处是如果不能立即执行递减操作而需要等待的话,调用此函数将返回错误(errno
设置为EAGAIN
)而不是阻塞。
【函数参数】
sem
:sem_t *
类型,表示需要等待的信号量。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
4.2.2 使用实例
暂无。
4.3 sem_trywait()
4.3.1 函数说明
在linux
下可以使用man 3 sem_trywait
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的P
操作。sem_timedwait()
与sem_wait()
相同,不同的是abs_timeout
指定了如果不能立即执行递减操作而需要等待的时间,超时将会返回。
【函数参数】
sem
:sem_t *
类型,表示需要等待的信号量。abs_timeout
:struct timespec
类型的结构体指针变量,它指向一个时间结构体,该时间结构体指定了自Epoch (1970-01-01 00:00:00 +0000 (UTC))
以来的以秒和纳秒为单位的绝对超时。
点击查看 struct timespec 结构体成员
1 | struct timespec |
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】如果可以立即执行操作,那么sem_timedwait()
永远不会出现超时错误,而不管abs_timeout
的值是多少。
4.3.2 使用实例
暂无。
5. 有名信号量的V
操作
V
操作就是释放资源,如果没有进程阻塞在该信号量上,表示没有进程等待该资源,这时该函数就对信号量的值进行加1
操作,表示同类资源多增加了一个。如果至少有一个进程阻塞在该信号量上,表示有进程等待资源,信号量为0
,这时该函数保持信号量为0
不变,并使某个阻塞在该信号量上的进程从sem_wait()
函数中返回。
5.1 sem_post()
5.1.1 函数说明
在linux
下可以使用man 3 sem_post
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的V
操作,也就是获取资源。
【函数参数】
sem
:sem_t *
类型,表示需释放的信号量。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
5.1.2 使用实例
暂无。
6. 使用实例
点击查看实例
1 | /* 头文件 */ |
1 | /* 头文件 */ |
1 | CC = gcc |
在终端执行以下命令编译程序,并分别在两个终端执行写入和读取的程序:
1 | make # 生成可执行文件 |
然后,终端会有以下信息显示:
1 | 执行 ./sem_write 的终端 |
三、POSIX 无名信号量
有名信号量和无名信号量的差异在于创建和销毁的形式上,其他的是一样的。无名信号量只能存在于内存中,所以就要求使用信号量的进程必须能访问信号量所在的这一块内存,所以无名信号量只能应用在同一进程内的线程之间(共享进程的内存)。另外,该信号量的使用需要借助于共享内存,打开或者创建之前需要进行以下步骤:
- (1)申请
key
; - (2)创建共享内存区域;
- (3)映射内存区域到进程的虚拟地址空间。
1. 初始化无名信号量
1.1 sem_init()
1.1.1 函数说明
在linux
下可以使用man 3 sem_init
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于初始化一个无名信号量。
【函数参数】
sem
:sem_t
类型,需要初始化的无名信号量名称。pshared
:int
类型,表示这个信号量是在进程的线程之间共享,还是在进程之间共享。一般指定为0
,表示这个无名信号量只能由初始化这个信号量的进程使用,不能在进程间使用,并且Linux
不支持使用无名信号量实现进程间的同步。value
:unsigned int
类型,表示无名信号量的初始值。该初始不能超过SEM_VALUE_MAX
,这个常值必须低于为32767
。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
表示错误。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.1.2 使用实例
暂无。
2. 销毁无名信号量
2.1 sem_destroy()
2.1.1 函数说明
在linux
下可以使用man 3 sem_destroy
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于销毁一个无名信号量。
【函数参数】
sem
:sem_t *
类型,需要关闭的信号量的地址。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
暂无。
3. 无名信号量的P
操作
与有名信号量的P
操作一样。
3.1 sem_wait()
3.1.1 函数说明
在linux
下可以使用man 3 sem_wait
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的P
操作,也就是获取资源。如果信号量的值大于0
,则信号量继续递减,函数立即返回;如果信号量当前的值是0
,那么调用就会阻塞,直到可以执行递减操作(即信号量的值升到0
以上),或者信号处理程序中断调用。
【函数参数】
sem
:sem_t *
类型,表示需要等待的信号量。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.1.2 使用实例
暂无。
3.2 sem_trywait()
3.2.1 函数说明
在linux
下可以使用man 3 sem_trywait
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的P
操作。sem_trywait()
与sem_wait()
相同,不同之处是如果不能立即执行递减操作而需要等待的话,调用此函数将返回错误(errno
设置为EAGAIN
)而不是阻塞。
【函数参数】
sem
:sem_t *
类型,表示需要等待的信号量。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.2.2 使用实例
暂无。
3.3 sem_trywait()
3.3.1 函数说明
在linux
下可以使用man 3 sem_trywait
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的P
操作。sem_timedwait()
与sem_wait()
相同,不同的是abs_timeout
指定了如果不能立即执行递减操作而需要等待的时间,超时将会返回。
【函数参数】
sem
:sem_t *
类型,表示需要等待的信号量。abs_timeout
:struct timespec
类型的结构体指针变量,它指向一个时间结构体,该时间结构体指定了自Epoch (1970-01-01 00:00:00 +0000 (UTC))
以来的以秒和纳秒为单位的绝对超时。
点击查看 struct timespec 结构体成员
1 | struct timespec |
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】如果可以立即执行操作,那么sem_timedwait()
永远不会出现超时错误,而不管abs_timeout
的值是多少。
3.3.2 使用实例
暂无。
4. 无名信号量的V
操作
与有名信号量的V
操作一样。
4.1 sem_post()
4.1.1 函数说明
在linux
下可以使用man 3 sem_post
命令查看该函数的帮助手册。
1 | /* Link with -pthread. */ |
【函数说明】该函数用于执行信号量的V
操作,也就是获取资源。
【函数参数】
sem
:sem_t *
类型,表示需释放的信号量。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
4.1.2 使用实例
暂无。
5. 使用实例
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall -lpthread # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | key=643d0b38 |
四、System V 信号量
1. 创建或打开信号量
1.1 semget()
1.1.1 函数说明
在linux
下可以使用man 2 semget
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于创建或者打开一个System V
信号量集,并获取信号量集的ID
。
【函数参数】
key
:key_t
类型,ftok
产生的key
值(和信号灯关联)或者IPC_PRIVATE
(这样只能用于具有血缘关系的进程通信)。nsems
:int
类型,信号量集中信号量的个数,当没有创建信号量集时,参数nems
可以为0
(表示不关心)。否则,nems
必须大于0
且小于等于每个信号量集(SEMMSL
)的最大信号量数。semflg
:int
类型,信号灯集的访问权限,我们一般设置为为IPC_CREAT |0666
。
点击查看 semflg 取值详情
如果key
值为IPC_PRIVATE
,或者没有现有的信号量集与key
相关联,并且在semflg
中指定了IPC_CREAT
,则会创建一组新的nems
信号量。
如果semflg
同时指定了IPC_CREAT
和IPC_EXCL
,并且key
的信号量集已经存在,那么semget()
将失败,errno
被设置为EEXIST
。
【返回值】int
类型,成功返回信号量集标识符(ID
),否则返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
1.1.2 使用实例
暂无。
2. 信号量P
、V
操作实现
在System V
信号量的相关函数中,没有直接实现P
、V
操作的函数,只有一个semop
函数,通过这个函数,。我们可以自己实现对信号量加1
和减1
操作。
2.1 semop()
2.1.1 函数说明
在linux
下可以使用man 2 semop
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数用于实现信号量的P
、V
操作。
【函数参数】
semid
:int
类型,已创建的信号量集的ID
。sops
:struct sembuf
类型的结构体指针变量,sops
所指向的信号集中的每一个nops
元素都是一个结构体,用于指定在单个信号量上执行的操作。
点击查看 struct sembuf 成员
1 | struct sembuf |
【注意事项】定义一个struct sembuf
类型的结构体变量,只能对某一个信号灯的操作,如果需要同时对多个信号量操作,则需要定义struct sembuf
结构体数组或者多个struct sembuf
结构体变量。
nsops
:size_t
类型,表示要进行操作的信号量的个数。
【返回值】int
类型,成功返回0
,失败返回-1
,并设置errno
。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
2.1.2 使用实例
暂无。
2.2 Poperation()
该函数为自定义的信号量的P
操作,通过semop
实现。
1 | /** |
2.3 Voperation()
该函数为自定义的信号量的V
操作,通过semop
实现。
1 | /** |
3. 信号量集控制
3.1 semctl()
3.1.1 函数说明
在linux
下可以使用man 2 semctl
命令查看该函数的帮助手册。
1 | /* 需包含的头文件 */ |
【函数说明】该函数对semid
标识的System V
信号量集或该信号量集的semnum
信号量执行cmd
指定的控制操作。(集合中的信号量从0
开始编号)。
【函数参数】
semid
:int
类型,已创建的信号量集的ID
。semnum
:int
类型,要操作的信号量集中的信号量编号。cmd
:int
类型,表示要执行的操作。
点击查看 cmd 常见取值及含义
GETVAL | 获取信号量的值,返回值是获得值。 |
SETVAL | 设置信号量的值,需要用到第四个参数,这个参数是一个共用体,后边会有介绍。 |
IPC_RMID | 从系统中删除信号量集合,唤醒所有因调用semop()阻塞在该信号量集合里的所有进程(相应调用会返回错误且errno被设置为EIDRM)。 |
arg
:union semun
类型,当cmd
为SETVAL
时才会有该参数,该参数用于设置信号集中信号量的参数,一般使用val
成员较多,该成员表示信号量的值。
点击查看 union semun 成员
1 | union semun |
【返回值】int
类型,成功时返回值受到cmd
的影响,一般会返回0
,失败都是返回-1
,并设置errno
,详情可查看man
手册。
【使用格式】一般情况下基本使用格式如下:
1 | /* 需要包含的头文件 */ |
【注意事项】none
3.1.2 使用实例
暂无。
4. 使用实例
需要提前说明的是,下边的例子只是为了测试System V
信号量,最后没有释放共享内存区域。
点击查看实例
1 | /* 头文件 */ |
在终端执行以下命令编译程序:
1 | gcc test.c -Wall # 生成可执行文件 a.out |
然后,终端会有以下信息显示:
1 | key=643d0b38 |
当我们使用Ctrl+c
结束进程后,使用ipcs -m
显示所有的共享内存IPC
对象信息,则有:
1 | ------------ 共享内存段 -------------- |
可以发现确实没有释放,但是关注点是在信号量的操作,所以这里我们其实可以手动删除共享内存,就是使用ipcrm -m shmid
命令