Skip to content

LV010-posix有名信号量

一、POSIX 有名信号量

这部分是关于有名信号量使用的笔记,需要注意的是有名信号量是随内核持续的,所以如果我们不调用sem_unlink来删除它,它将会一直存在,直到内核重启。另外,该信号量的使用需要借助于共享内存,打开或者创建之前需要进行以下步骤:

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

1. 创建或打开有名信号量

1.1 sem_open()

1.1.1 函数说明

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

c
/* 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查看该参数详细的说明:

shell
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 参数取值如下:
O_CREAT如果信号量不存在,就会创建信号量并初始化,函数第三个和第四个参数是必要的的。
O_CREAT|O_EXCL如果给定名称的信号量已经存在,则返回一个错误。
我是查看了`man`手册,大概就是这两点吧。
  • modemode_t类型,表示文件权限,常用参数为0666
  • valueunsigned int 类型,表示信号量的初始值。该初始不能超过SEM_VALUE_MAX,这个常值必须低于为32767。二值信号量的初始值通常为1,计数信号量的初始值则往往大于1

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

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

c
/* 需要包含的头文件 */
#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命令查看该函数的帮助手册。

c
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

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

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

函数参数

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

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

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

c
/* 需要包含的头文件 */
#include <semaphore.h>

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

注意事项none

2.1.2 使用实例

暂无。

3. 删除有名信号量

3.1.1 函数说明

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

c
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

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

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

函数参数

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

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

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

c
/* 需要包含的头文件 */
#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命令查看该函数的帮助手册。

c
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

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

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

函数参数

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

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

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

c
/* 需要包含的头文件 */
#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命令查看该函数的帮助手册。

c
/* 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

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

c
/* 需要包含的头文件 */
#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命令查看该函数的帮助手册。

c
/* 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 结构体成员如下:
c
struct timespec 
{
	time_t tv_sec;      /* Seconds */
	long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
};

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

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

c
/* 需要包含的头文件 */
#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命令查看该函数的帮助手册。

c
/* Link with -pthread. */
/* 需包含的头文件 */
#include <semaphore.h>

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

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

函数参数

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

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

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

c
/* 需要包含的头文件 */
#include <semaphore.h>

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

注意事项none

5.1.2 使用实例

暂无。

二、使用实例

1. sem_write.c

c
/* 头文件 */
#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);
}

2. sem_read.c

c
/* 头文件 */
#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);
}

3. Makefile

makefile
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

3. 测试效果

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

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

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

shell
# 执行 ./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!