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
2
3
4
5
6
7
8
9
10
信号量的值 - 1;
if(信号量的值 ≥ 0)
{
申请资源的任务继续运行;
}
else
{
申请资源的任务阻塞,并插入等待队列;
}

2.2 V 操作(释放资源)

V 操作会把信号量加上 1,相加后如果信号量 <= 0,则表明当前有阻塞中的进程,于是会从等待队列中取出一个进程唤醒运行;相加后如果信号量 > 0,则表明当前没有阻塞中的进程。也就是

1
2
3
4
5
信号量的值 + 1;
if (信号量的值 <= 0)
{
唤醒等待队列的一个任务,让其继续运行;
}

3. 互斥信号量

上边的已经了解了信号量的操作,那具体是怎样工作的呢?比如我们现在有两个进程:进程1和进程2,还有一个信号量sem,我们将信号量sem初始化为1然后进程1和进程2都要访问共享内存。

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

如果进程 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
2
3
4
5
6
7
8
9
/* Link with -pthread. */
/* 需包含的头文件 */
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>

/* 函数声明 */
sem_t *sem_open(const char *name, int oflag);
sem_t *sem_open(const char *name, int oflag, mode_t mode, unsigned int value);

【函数说明】该函数用于打开或者创建一个有名信号量。

【函数参数】

  • namechar *类型,该参数表示信号量的名字(最好不要包含路径,可能会报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路径下,也不需要我们来指定路径。

  • oflagint类型,选择创建或者打开一个信号量,由于此函数会创建有名信号量文件,所以这里一般可以通过|与表示文件存取权限的宏(如O_RDWR)一起使用。 一般选择O_CREAT|O_RDWR参数,表示以可读写的方式创建并初始化一个信号量,此时modevalue参数是被需要的。如果指定了O_CREAT,并且给定名称的信号量已经存在,那么modevalue将被忽略。
点击查看 oflag 参数取值

我是查看了man手册,大概就是这两点吧。

O_CREAT如果信号量不存在,就会创建信号量并初始化,函数第三个和第四个参数是必要的的。
O_CREAT|O_EXCL如果给定名称的信号量已经存在,则返回一个错误。
  • modemode_t类型,表示文件权限,常用参数为0666
  • valueunsigned int 类型,表示信号量的初始值。该初始不能超过SEM_VALUE_MAX,这个常值必须低于为32767。二值信号量的初始值通常为1,计数信号量的初始值则往往大于1

【返回值】sem_t *类型,成功返回新信号量的地址,失败返回SEM_FAILED,并设置errno表示错误。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_r = sem_open("mysem_r", O_CREAT|O_RDWR, 0666, 0);

【注意事项】创建的信号量文件在/dev/shm目录下。

1.1.2 使用实例

暂无。

2. 关闭有名信号量

2.1 sem_close() 

2.1.1 函数说明

linux下可以使用man 3 sem_close命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_close(sem_t *sem);

【函数说明】该函数用于关闭一个信号量。

【函数参数】

  • semsem_t *类型,需要关闭的信号量的地址。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_close(sem_r);

【注意事项】none

2.1.2 使用实例

暂无。

3. 删除有名信号量

3.1.1 函数说明

linux下可以使用man 3 sem_unlink命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_unlink(const char *name);

【函数说明】该函数用于删除一个有名信号量。

【函数参数】

  • namechar *类型,表示需要删除的有名信号量的名称,若名称并非使用变量指定,可以直接将名称用" "包裹即可。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_unlink("sem_name");

【注意事项】当我们一个进程运行完毕的时候,信号量是一直存在的,当我们下一次重新运行这个进程,由于信号量存在,这就可能会导致出现一些问题,所以我们一般会自定义一个信号处理函数,在信号处理函数中使用该删除函数删除有名信号量。

3.1.2 使用实例

暂无。

4. 有名信号量的P操作

P操作其实就是获取资源,如果信号量为0,表示这时没有相应资源空闲,那么调用进程就将挂起,直到有空闲资源可以获取。

4.1 sem_wait() 

4.1.1 函数说明

linux下可以使用man 3 sem_wait命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_wait(sem_t *sem);

【函数说明】该函数用于执行信号量的P操作,也就是获取资源。如果信号量的值大于0,则信号量继续递减,函数立即返回;如果信号量当前的值是0,那么调用就会阻塞,直到可以执行递减操作(即信号量的值升到0以上),或者信号处理程序中断调用。

【函数参数】

  • semsem_t *类型,表示需要等待的信号量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_wait(sem_r);

【注意事项】none

4.1.2 使用实例

暂无。

4.2 sem_trywait() 

4.2.1 函数说明

linux下可以使用man 3 sem_trywait命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_trywait(sem_t *sem);

【函数说明】该函数用于执行信号量的P操作。sem_trywait()sem_wait()相同,不同之处是如果不能立即执行递减操作而需要等待的话,调用此函数将返回错误(errno设置为EAGAIN)而不是阻塞。

【函数参数】

  • semsem_t *类型,表示需要等待的信号量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_trywait(sem_r);

【注意事项】none

4.2.2 使用实例

暂无。

4.3 sem_trywait()

4.3.1 函数说明

linux下可以使用man 3 sem_trywait命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

【函数说明】该函数用于执行信号量的P操作。sem_timedwait()sem_wait()相同,不同的是abs_timeout指定了如果不能立即执行递减操作而需要等待的时间,超时将会返回。

【函数参数】

  • semsem_t *类型,表示需要等待的信号量。
  • abs_timeoutstruct timespec类型的结构体指针变量,它指向一个时间结构体,该时间结构体指定了自Epoch (1970-01-01 00:00:00 +0000 (UTC))以来的以秒和纳秒为单位的绝对超时。
点击查看 struct timespec 结构体成员
1
2
3
4
5
struct timespec 
{
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
struct timespec tout;
sem_timedwait(sem_r, &tout);

【注意事项】如果可以立即执行操作,那么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
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_post(sem_t *sem);

【函数说明】该函数用于执行信号量的V操作,也就是获取资源。

【函数参数】

  • semsem_t *类型,表示需释放的信号量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_post(sem_r);

【注意事项】none

5.1.2 使用实例

暂无。

6. 使用实例

点击查看实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
/* 头文件 */
#include <stdio.h> /* perror fgets shmat*/
#include <sys/shm.h> /* shmget shmat*/
#include <sys/ipc.h> /* shmget ftok */
#include <string.h> /* strcpy */
#include <stdlib.h> /* system */
#include <signal.h> /* sigaction sigemptyset */


#include <fcntl.h> /* sem_open For O_* constants */
#include <sys/stat.h> /* sem_open For mode constants */
#include <semaphore.h>/* sem_open sem_post sem_unlink */

void deleteSemfile(int sig);
/* 主函数 */
int main(int argc, char *argv[])
{
key_t key;
int shmid; /* 共享内存ID */
char *shmAddr;/* 共享内存首地址 */
struct sigaction act; /* 处理信号行为的结构体变量 */
sem_t *sem_r, *sem_w; /* 定义两个信号量 */

/* 1. 生成key */
key = ftok("./key.txt", 100); /* 需要提前创建一个key.txt文件 */
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key=%x\n", key);
/* 2. 创建或者打开共享内存 */
shmid = shmget(key, 512, IPC_CREAT|0666);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid=%d\n", shmid);
/* 3. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问*/
shmAddr = shmat(shmid, NULL, 0);
if(shmAddr < 0)
{
perror("shmat");
return -1;
}
printf("shmAddr=%p\n", shmAddr);
/* 4. 信号捕获 */
act.sa_handler = deleteSemfile; /* 指定信号处理函数 */
act.sa_flags = 0; /* sa_flags指定了一组标志,这些标志用于控制信号的处理过程 */
sigemptyset(&act.sa_mask); /* 将信号集初始化为空 */
sigaction(SIGINT, &act, NULL); /* 捕捉信号 Ctrl+c 发出*/
/* 5. 信号量初始化(会在/dev/shm下创建相应的两个文件) */
sem_r = sem_open("mysem_r", O_CREAT|O_RDWR, 0666, 0);
sem_w = sem_open("mysem_w", O_CREAT|O_RDWR, 0666, 1);
/* 注意:程序执行完毕后,上边两个文件被创建,重新执行两个进程程序的时候信号量已经存在,此时无法正常通信,需要删除两个文件 */
/* 6. 写入数据 */
while(1)
{
sem_wait(sem_w);/* 获取资源 */
printf("Please enter data:");
fgets(shmAddr, 500, stdin);
sem_post(sem_r);/* 释放资源 */
}

return 0;
}

/* 删除 /dev/shm/mysem_w 文件*/
void deleteSemfile(int sig)
{
sem_unlink("mysem_w");
printf("Delete mysem_w!\n");
exit(0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
/* 头文件 */
#include <stdio.h> /* perror fgets shmat*/
#include <sys/shm.h> /* shmget shmat*/
#include <sys/ipc.h> /* shmget ftok */
#include <string.h> /* strcpy */
#include <stdlib.h> /* system */
#include <signal.h> /* sigaction sigemptyset */

#include <fcntl.h> /* sem_open For O_* constants */
#include <sys/stat.h> /* sem_open For mode constants */
#include <semaphore.h>/* sem_open sem_post */

void deleteSemfile(int sig);

/* 主函数 */
int main(int argc, char *argv[])
{
key_t key; /* key */
int shmid; /* 共享内存ID */
char *shmAddr; /* 共享内存首地址 */
struct sigaction act; /* 处理信号行为的结构体变量 */
sem_t *sem_r, *sem_w; /* 定义两个信号量 */

/* 1. 生成key */
key = ftok("./key.txt", 100);/* 需要提前创建一个key.txt文件 */
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key=%x\n", key);
/* 2. 创建或者打开共享内存 */
shmid = shmget(key, 512, IPC_CREAT|0666);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid=%d\n", shmid);
/* 3. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问*/
shmAddr = shmat(shmid, NULL, 0);
if(shmAddr < 0)
{
perror("shmat");
return -1;
}
printf("shmAddr=%p\n", shmAddr);
/* 4. 信号捕获 */
act.sa_handler = deleteSemfile; /* 指定信号处理函数 */
act.sa_flags = 0; /* sa_flags指定了一组标志,这些标志用于控制信号的处理过程 */
sigemptyset(&act.sa_mask); /* 将信号集初始化为空 */
sigaction(SIGINT, &act, NULL); /* 捕捉信号 Ctrl+c 发出*/
/* 5. 信号量初始化(会在/dev/shm下创建相应的两个文件) */
sem_r = sem_open("mysem_r", O_CREAT|O_RDWR, 0666, 0);
sem_w = sem_open("mysem_w", O_CREAT|O_RDWR, 0666, 1);
/* 注意:程序执行完毕后,上边两个文件被创建,重新执行两个进程程序的时候信号量已经存在,此时无法正常通信,需要删除两个文件 */
/* 6. 写入数据 */
while(1)
{
sem_wait(sem_r);/* 获取资源 */
printf("read data:%s\n", shmAddr);
sem_post(sem_w);/* 释放资源 */
}

return 0;
}

/* 删除 /dev/shm/mysem_r 文件*/
void deleteSemfile(int sig)
{
sem_unlink("mysem_r");
printf("Delete mysem_r!\n");
exit(0);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
CC = gcc

DEBUG = -g -O2
CFLAGS = -Wall
LIB = -l pthread
# 所有.c文件去掉后缀
OBJ_LIST = ${patsubst %.c, %.o, ${wildcard *.c}}
TARGET_LIST = ${patsubst %.c, %, ${wildcard *.c}}

all : $(OBJ_LIST)
@for var in $(TARGET_LIST); \
do \
$(CC) $$var.o -o $$var $(LIB); \
echo $(CC) $$var.o -o $$var $(LIB);\
done

# compile
%.o : %.c
$(CC) $(DEBUG) -c $(CFLAGS) $< -o $@


.PHONY: all clean clean_o
clean : clean_o
@rm -vf $(TARGET_LIST)


clean_o :
@rm -vf *.o

在终端执行以下命令编译程序,并分别在两个终端执行写入和读取的程序:

1
2
3
make # 生成可执行文件
./sem_write # 共享内存写入
./sem_read # 共享内存读取

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 执行 ./sem_write 的终端
hk@vm:~/2Sharedfiles/5temp$ ./sem_write
key=643d0b4e
shmid=17
shmAddr=0x7f3d7a36c000
Please enter data:qwert
Please enter data:^CDelete mysem_w!

# 执行 ./sem_read 的终端
hk@vm:/mnt/hgfs/Sharedfiles/5temp$ ./sem_read
key=643d0b4e
shmid=17
shmAddr=0x7f661a6ad000
read data:qwert

^CDelete mysem_r!

三、POSIX 无名信号量

有名信号量和无名信号量的差异在于创建和销毁的形式上,其他的是一样的。无名信号量只能存在于内存中,所以就要求使用信号量的进程必须能访问信号量所在的这一块内存,所以无名信号量只能应用在同一进程内的线程之间(共享进程的内存)。另外,该信号量的使用需要借助于共享内存,打开或者创建之前需要进行以下步骤:

  • (1)申请key
  • (2)创建共享内存区域;
  • (3)映射内存区域到进程的虚拟地址空间。

1. 初始化无名信号量

1.1 sem_init() 

1.1.1 函数说明

linux下可以使用man 3 sem_init命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_init(sem_t *sem, int pshared, unsigned int value);

【函数说明】该函数用于初始化一个无名信号量。

【函数参数】

  • semsem_t类型,需要初始化的无名信号量名称。

  • psharedint类型,表示这个信号量是在进程的线程之间共享,还是在进程之间共享。一般指定为0,表示这个无名信号量只能由初始化这个信号量的进程使用,不能在进程间使用,并且Linux 不支持使用无名信号量实现进程间的同步。

  • valueunsigned int 类型,表示无名信号量的初始值。该初始不能超过SEM_VALUE_MAX,这个常值必须低于为32767

【返回值】int类型,成功返回0,失败返回-1,并设置errno表示错误。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <fcntl.h> /* For O_* constants */
#include <sys/stat.h> /* For mode constants */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t sem_r; /* 定义成全局变量 */
sem_init(&sem_r, 0, 0);

【注意事项】none

1.1.2 使用实例

暂无。

2. 销毁无名信号量

2.1 sem_destroy() 

2.1.1 函数说明

linux下可以使用man 3 sem_destroy命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_destroy(sem_t *sem);

【函数说明】该函数用于销毁一个无名信号量。

【函数参数】

  • semsem_t *类型,需要关闭的信号量的地址。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;/* 定义成全局变量 */
sem_destroy(sem_r);

【注意事项】none

2.1.2 使用实例

暂无。

3. 无名信号量的P操作

与有名信号量的P操作一样。

3.1 sem_wait()

3.1.1 函数说明

linux下可以使用man 3 sem_wait命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_wait(sem_t *sem);

【函数说明】该函数用于执行信号量的P操作,也就是获取资源。如果信号量的值大于0,则信号量继续递减,函数立即返回;如果信号量当前的值是0,那么调用就会阻塞,直到可以执行递减操作(即信号量的值升到0以上),或者信号处理程序中断调用。

【函数参数】

  • semsem_t *类型,表示需要等待的信号量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_wait(sem_r);

【注意事项】none

3.1.2 使用实例

暂无。

3.2 sem_trywait() 

3.2.1 函数说明

linux下可以使用man 3 sem_trywait命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_trywait(sem_t *sem);

【函数说明】该函数用于执行信号量的P操作。sem_trywait()sem_wait()相同,不同之处是如果不能立即执行递减操作而需要等待的话,调用此函数将返回错误(errno设置为EAGAIN)而不是阻塞。

【函数参数】

  • semsem_t *类型,表示需要等待的信号量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_trywait(sem_r);

【注意事项】none

3.2.2 使用实例

暂无。

3.3 sem_trywait() 

3.3.1 函数说明

linux下可以使用man 3 sem_trywait命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

【函数说明】该函数用于执行信号量的P操作。sem_timedwait()sem_wait()相同,不同的是abs_timeout指定了如果不能立即执行递减操作而需要等待的时间,超时将会返回。

【函数参数】

  • semsem_t *类型,表示需要等待的信号量。
  • abs_timeoutstruct timespec类型的结构体指针变量,它指向一个时间结构体,该时间结构体指定了自Epoch (1970-01-01 00:00:00 +0000 (UTC))以来的以秒和纳秒为单位的绝对超时。
点击查看 struct timespec 结构体成员
1
2
3
4
5
struct timespec 
{
time_t tv_sec; /* Seconds */
long tv_nsec; /* Nanoseconds [0 .. 999999999] */
};

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
struct timespec tout;
sem_timedwait(sem_r, &tout);

【注意事项】如果可以立即执行操作,那么sem_timedwait()永远不会出现超时错误,而不管abs_timeout的值是多少。

3.3.2 使用实例

暂无。

4. 无名信号量的V操作

与有名信号量的V操作一样。

4.1 sem_post() 

4.1.1 函数说明

linux下可以使用man 3 sem_post命令查看该函数的帮助手册。

1
2
3
4
5
6
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

/* 函数声明 */
int sem_post(sem_t *sem);

【函数说明】该函数用于执行信号量的V操作,也就是获取资源。

【函数参数】

  • semsem_t *类型,表示需释放的信号量。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
/* 需要包含的头文件 */
#include <semaphore.h>

/* 至少应该有的语句 */
sem_t *sem_r;
sem_post(sem_r);

【注意事项】none

4.1.2 使用实例

暂无。

5. 使用实例

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
/* 头文件 */
#include <stdio.h> /* perror fgets shmat*/
#include <sys/shm.h> /* shmget shmat*/
#include <sys/ipc.h> /* shmget ftok */
#include <string.h> /* strcpy */
#include <stdlib.h> /* system */
#include <signal.h> /* sigaction sigemptyset */


#include <fcntl.h> /* sem_init For O_* constants */
#include <sys/stat.h> /* sem_init For mode constants */
#include <semaphore.h>/* sem_init sem_post */
#include <pthread.h> /* pthread_create */


sem_t sem_r, sem_w; /* 定义两个信号量 */
char *shmAddr; /* 共享内存首地址 */

void *readSem(void *arg); /* 读取数据线程 */
void deleteSemfile(int sig);/* 删除无名信号量 */

/* 主函数 */
int main(int argc, char *argv[])
{
key_t key;
int shmid;
struct sigaction act;/* 处理信号行为的结构体变量 */
pthread_t tid;
/* 1. 生成key */
key = ftok("./key.txt", 100);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key=%x\n", key);
/* 2. 创建或者打开共享内存 */
shmid = shmget(key, 512, IPC_CREAT|0666);
if(shmid < 0)
{
perror("shmget");
return 0;
}
printf("shmid=%d\n", shmid);
/* 3. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问*/
shmAddr = shmat(shmid, NULL, 0);
if(shmAddr < 0)
{
perror("shmat");
return 0;
}
printf("shmAddr=%p\n", shmAddr);
/* 4. 信号捕获 */
act.sa_handler = deleteSemfile; /* 指定信号处理函数 */
act.sa_flags = 0; /* sa_flags指定了一组标志,这些标志用于控制信号的处理过程 */
sigemptyset(&act.sa_mask); /* 将信号集初始化为空 */
sigaction(SIGINT, &act, NULL); /* 捕捉信号 */
/* 5. 信号量初始化) */
sem_init(&sem_r, 0, 0);
sem_init(&sem_w, 0, 1);
/* 6. 创建线程 */
pthread_create(&tid, NULL, readSem, NULL);
/* 7. 写入数据 */
while(1)
{
sem_wait(&sem_w);/* 获取资源 */
printf(">");
fgets(shmAddr, 500, stdin);
sem_post(&sem_r);/* 释放资源 */
}

return 0;
}

/* 线程函数 */
void *readSem(void *arg)
{
while(1)
{
sem_wait(&sem_r);
printf("%s\n",shmAddr);
sem_post(&sem_w);

}

}

/* 删除信号量 */
void deleteSemfile(int sig)
{
sem_destroy(&sem_r);
sem_destroy(&sem_w);
printf("Destroy sem_r and sem_w!\n");
exit(0);
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall -lpthread # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
key=643d0b38
shmid=18
shmAddr=0x7f421bd53000
>qwert
qwert

>^CDestroy sem_r and sem_w!

四、System V 信号量

1. 创建或打开信号量

1.1 semget() 

1.1.1 函数说明

linux下可以使用man 2 semget命令查看该函数的帮助手册。

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 函数声明 */
int semget(key_t key, int nsems, int semflg);

【函数说明】该函数用于创建或者打开一个System V信号量集,并获取信号量集的ID

【函数参数】

  • keykey_t类型,ftok产生的key值(和信号灯关联)或者IPC_PRIVATE (这样只能用于具有血缘关系的进程通信)。
  • nsemsint类型,信号量集中信号量的个数,当没有创建信号量集时,参数nems可以为0(表示不关心)。否则,nems必须大于0且小于等于每个信号量集(SEMMSL)的最大信号量数。
  • semflgint类型,信号灯集的访问权限,我们一般设置为为IPC_CREAT |0666
点击查看 semflg 取值详情

如果key值为IPC_PRIVATE,或者没有现有的信号量集与key相关联,并且在semflg中指定了IPC_CREAT,则会创建一组新的nems信号量。

如果semflg同时指定了IPC_CREATIPC_EXCL,并且key的信号量集已经存在,那么semget()将失败,errno被设置为EEXIST

【返回值】int类型,成功返回信号量集标识符(ID),否则返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
/* 需要包含的头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 至少应该有的语句 */
int semid;
semid = semget(key, 2, IPC_CREAT|0666);

【注意事项】none

1.1.2 使用实例

暂无。

2. 信号量PV操作实现

System V信号量的相关函数中,没有直接实现PV操作的函数,只有一个semop函数,通过这个函数,。我们可以自己实现对信号量加1和减1操作。

2.1 semop() 

2.1.1 函数说明

linux下可以使用man 2 semop命令查看该函数的帮助手册。

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 函数声明 */
int semop(int semid, struct sembuf *sops, size_t nsops);

【函数说明】该函数用于实现信号量的PV操作。

【函数参数】

  • semidint类型,已创建的信号量集的ID
  • sopsstruct sembuf类型的结构体指针变量,sops所指向的信号集中的每一个nops元素都是一个结构体,用于指定在单个信号量上执行的操作。
点击查看 struct sembuf 成员
1
2
3
4
5
6
7
struct sembuf 
{
short sem_num; /* 要操作的信号灯的编号 */
short sem_op; /* 1,释放资源,V操作; -1,分配资源,P操作 */
short sem_flg; /* 0(阻塞), IPC_NOWAIT, SEM_UNDO */
};

【注意事项】定义一个struct sembuf类型的结构体变量,只能对某一个信号灯的操作,如果需要同时对多个信号量操作,则需要定义struct sembuf结构体数组或者多个struct sembuf结构体变量。

  • nsopssize_t类型,表示要进行操作的信号量的个数。

【返回值】int类型,成功返回0,失败返回-1,并设置errno

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
10
11
12
/* 需要包含的头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 至少应该有的语句 */
struct sembuf sbuf;
sbuf.sem_num = 0;
sbuf.sem_op = 1;
sbuf.sem_flg = 0;

semop(semid, &sbuf, 1);

【注意事项】none

2.1.2 使用实例

暂无。

2.2 Poperation() 

该函数为自定义的信号量的P操作,通过semop实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Function: Poperation
* @Description: System V 信号量P操作,也就是获取资源
* @param semid : 已经创建的信号量集ID
* @param semindex : 信号量在信号量集中的编号(索引)
* @return : none
*/
void Poperation(int semid, int semindex)
{
struct sembuf sbuf;
sbuf.sem_num = semindex;
sbuf.sem_op = -1;
sbuf.sem_flg = 0;

semop(semid, &sbuf, 1);
}

2.3 Voperation() 

该函数为自定义的信号量的V操作,通过semop实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @Function: Voperation
* @Description: System V 信号量V操作,也就是释放资源
* @param semid : 已经创建的信号量集ID
* @param semindex : 信号量在信号量集中的编号(索引)
* @return : none
*/
void Voperation(int semid,int semindex)
{
struct sembuf sbuf;
sbuf.sem_num = semindex;
sbuf.sem_op = 1;
sbuf.sem_flg = 0;

semop(semid, &sbuf, 1);
}

3. 信号量集控制

3.1 semctl() 

3.1.1 函数说明

linux下可以使用man 2 semctl 命令查看该函数的帮助手册。

1
2
3
4
5
6
7
8
/* 需包含的头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 函数声明 */
int semctl(int semid, int semnum, int cmd, ...);/* man手册函数声明 */
int semctl(int semid, int semnum, int cmd, ... /* union semun arg */ );/* 我自己加的 */

【函数说明】该函数对semid标识的System V信号量集或该信号量集的semnum信号量执行cmd指定的控制操作。(集合中的信号量从0开始编号)。

【函数参数】

  • semidint类型,已创建的信号量集的ID
  • semnumint类型,要操作的信号量集中的信号量编号。
  • cmdint类型,表示要执行的操作。
点击查看 cmd 常见取值及含义
GETVAL获取信号量的值,返回值是获得值。
SETVAL设置信号量的值,需要用到第四个参数,这个参数是一个共用体,后边会有介绍。
IPC_RMID从系统中删除信号量集合,唤醒所有因调用semop()阻塞在该信号量集合里的所有进程(相应调用会返回错误且errno被设置为EIDRM)。
  • argunion semun类型,当cmdSETVAL时才会有该参数,该参数用于设置信号集中信号量的参数,一般使用val成员较多,该成员表示信号量的值。
点击查看 union semun 成员
1
2
3
4
5
6
7
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类型,成功时返回值受到cmd的影响,一般会返回0,失败都是返回-1,并设置errno,详情可查看man手册。

【使用格式】一般情况下基本使用格式如下:

1
2
3
4
5
6
7
8
9
/* 需要包含的头文件 */
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

/* 至少应该有的语句 */
union semun mysem;/* 信号量参数结构体 */
mysem.val = 0; /* 设置信号量的值 */
semctl(semid, SEM_READ, SETVAL, mysem); /* 初始化信号量 */

【注意事项】none

3.1.2 使用实例

暂无。

4. 使用实例

需要提前说明的是,下边的例子只是为了测试System V信号量,最后没有释放共享内存区域。

点击查看实例
test.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
/* 头文件 */
#include <stdio.h> /* perror fgets */
#include <sys/shm.h> /* shmget shmat */
#include <sys/ipc.h> /* semop semget ftok shmget shmat semctl */
#include <sys/sem.h> /* semop semget semctl */
#include <stdlib.h> /* system */
#include <signal.h> /* sigaction sigemptyset */
#include <sys/types.h>/* fork */
#include <unistd.h> /* fork */

#define SEM_READ 0 /* 读信号量编号 */
#define SEM_WRITE 1 /* 写信号量编号 */

union semun
{
int val;
};

void Poperation(int semid,int semindex);/* 获取资源 */
void Voperation(int semid,int semindex);/* 释放资源 */

/* 主函数 */
int main(int argc, char *argv[])
{
key_t key;
int shmid, semid;
char *shmAddr;
union semun mysem;/* 信号灯参数结构体 */
pid_t pid;
/* 1. 生成key */
key = ftok("./key.txt", 100);
if(key < 0)
{
perror("ftok");
return -1;
}
printf("key=%x\n", key);
/* 2. 创建或者打开共享内存 */
shmid = shmget(key, 512, IPC_CREAT|0666);
if(shmid < 0)
{
perror("shmget");
return -1;
}
printf("shmid=%d\n", shmid);
/* 3. 映射共享内存,即把指定的共享内存映射到进程的地址空间用于访问*/
shmAddr = shmat(shmid, NULL, 0);
if(shmAddr < 0)
{
perror("shmat");
return -1;
}
printf("shmAddr=%p\n", shmAddr);
/* 4. 创建或者打开信号量集 */
semid = semget(key, 2, IPC_CREAT|0666);/* 信号量集包含两个信号量 */
if(semid < 0)
{
perror("semget");
return -1;
}
/* 5. 初始化读写信号量 */
mysem.val = 0;
semctl(semid, SEM_READ, SETVAL, mysem);
mysem.val = 1;
semctl(semid, SEM_WRITE, SETVAL, mysem);

/* 6. 创建父子进程 */
pid = fork();
if(pid < 0) /* 创建出错 */
{
perror("fork");

shmctl(shmid, IPC_RMID, NULL);/* 释放共享内存 */
semctl(semid, 0, IPC_RMID); /* 删除信号量 */

exit(-1);
}
else if(pid == 0) /* 子进程 读信号量 */
{
while(1)
{
Poperation(semid, SEM_READ);
printf("%s\n", shmAddr);

Voperation(semid, SEM_WRITE);
}
}
else /* 父进程 写信号量*/
{
while(1)
{
Poperation(semid, SEM_WRITE);
printf(">");
fgets(shmAddr, 32, stdin);

Voperation(semid, SEM_READ);
}
}
return 0;
}

/**
* @Function: Poperation
* @Description: System V 信号量P操作,也就是获取资源
* @param semid : 已经创建的信号量集ID
* @param semindex : 信号量在信号量集中的编号(索引)
* @return : none
*/
void Poperation(int semid,int semindex)
{
struct sembuf sbuf;
sbuf.sem_num = semindex;
sbuf.sem_op = -1;
sbuf.sem_flg = 0;

semop(semid, &sbuf, 1);
}
/**
* @Function: Poperation
* @Description: System V 信号量V操作,也就是释放资源
* @param semid : 已经创建的信号量集ID
* @param semindex : 信号量在信号量集中的编号(索引)
* @return : none
*/
void Voperation(int semid,int semindex)
{
struct sembuf sbuf;
sbuf.sem_num = semindex;
sbuf.sem_op = 1;
sbuf.sem_flg = 0;

semop(semid, &sbuf, 1);
}

在终端执行以下命令编译程序:

1
2
gcc test.c -Wall # 生成可执行文件 a.out 
./a.out # 执行可执行程序

然后,终端会有以下信息显示:

1
2
3
4
5
6
7
8
key=643d0b38
shmid=18
shmAddr=0x7f92372a9000
>qidaink
qidaink

>

当我们使用Ctrl+c结束进程后,使用ipcs -m显示所有的共享内存IPC对象信息,则有:

1
2
3
------------ 共享内存段 --------------
键 shmid 拥有者 权限 字节 连接数 状态
0x643d0b38 18 hk 666 512 0

可以发现确实没有释放,但是关注点是在信号量的操作,所以这里我们其实可以手动删除共享内存,就是使用ipcrm -m shmid命令