Linux C语言 38-进程间通信IPC之信号量
本节关键字:C语言 进程间通信 信号量 semaphore
相关库函数:sem_init、sem_post、sem_wait、sem_destroy、sem_open、sem_close、sem_unlink
什么是信号量?
信号量(信号灯)本质是一个计数器,是描述临界资源中资源数目的计数器,信号量能够更细粒度的对临界资源进行管理。
信号量的原理
信号量是包含一个非负整数型的变量,并且带有两个原子操作wait和signal。
- wait还被称为down、P或lock;
- signal还被称为up、V、unlock或post;
- 在UNIX的API中(POSIX标准)用的是wait和post。
对于wait操作,如果信号量的非负整形变量S大于0,wait就将其减1,如果S等于0,wait就将调用线程阻塞;对于post操作,如果有线程在信号量上阻塞(此时S等于0),post就会解除对某个等待线程的阻塞,使其从wait中返回,如果没有线程阻塞在信号量上,post就将S加1。
由此可见,信号量S可以被理解为一种资源的数量,信号量即是通过控制这种资源的分配来实现互斥和同步的。如果把S设为1,那么信号量即可使多线程并发运行。另外,信号量不仅允许使用者申请和释放资源,而且还允许使用者创造资源,这就赋予了信号量实现同步的功能。可见信号量的功能要比互斥量丰富许多。
信号量的分类
无名信号量
无名信号量只可以在共享内存的情况下,比如实现进程中各个线程之间的互斥和同步,因此无名信号量也被称作基于内存的信号量。
命名信号量
命名信号量有一个名字、一个用户ID、一个组ID和权限,这些参数为不共享内存的进程提供使用命名信号量的接口。命名信号量通常用于不共享内存的情况下,比如进程间通信。
在创建信号量时,根据信号量取值的不同,POSIX信号量还可以分为:
- 二值信号量:信号量的值只有0和1,感觉和互斥量很类似。资源被锁住时,信号量的值为0;资源可用时,信号量的值为1。
- 计数信号量:信号量的值在0到一个大于1的限制值之间,该值可以表示可用资源的个数。
无名信号量相关库函数
编译时需要连接动态库 -lrt 或 -pthread
初始化信号量
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
/**
@brief 初始化参数sem指向的信号量,设置共享选项,并设置信号量的初始值。重复初始化会导致未定义的行为。
@param sem 指向一个将被初始化的信号量
@param pshared 信号量的类型,
=0 表示信号量在当前进程的线程之间共享,并且应该位于所有线程都可见的某个地址;
!=0 表示信号量在进程之间共享,并且应该位于共享内存的区域中,任何可以访问共享内存的进程都可以使用sem_post()、sem_wait()等对信号量进行操作
@param value 信号量将被设定的初始值
@return 成功返回0,失败返回-1并设置错误码error
错误码error类型:
EINVAL 值超过SEM_value_MAX
ENOSYS pshared为非零,但系统不支持进程共享信号量(请参阅sem_overview(7))
*/
递减(锁定)信号量
#include <semaphore.h>
int sem_wait(sem_t *sem);
/**
@brief sem_wait()递减(锁定)sem指向的信号量。如果信号量的值大于零,则递减继续,函数立即返回。如果信号量当前的值为零,那么调用将阻塞,直到可以执行递减(即信号量值上升到零以上),或者信号处理程序中断调用。
@param sem 指向将被操作的信号量
@return 成功返回0,失败返回-1并设置错误码error
*/
int sem_trywait(sem_t *sem);
/**
@brief sem_trywait()与sem_wait()相同,只是如果不能立即执行递减,则调用返回一个错误(errno设置为EAGAIN),而不是阻塞。
@param sem 指向将被操作的信号量
@return 成功返回0,失败返回-1并设置错误码error
*/
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
/**
@brief timedwait()与sem_wait()相同,除了abs_timeout指定了在不能立即执行递减时调用应该阻止的时间量的限制。如果在调用时超时已经过期,并且信号量无法立即锁定,则sem_timedwait()将失败并出现超时错误(errno设置为ETIMEDOUT)。如果该操作可以立即执行,那么无论abs_timeout的值如何,sem_timedwait()都不会因超时错误而失败。此外,在这种情况下,不检查abs_timeout的有效性。
@param sem 指向将被操作的信号量
@param abs_timeout 指向一个结构,该结构指定自Epoch(1970年1月1日00:00:00)以来的绝对超时(以秒和纳秒为单位)。该结构定义如下:
struct timespec {
time_t tv_sec; // Seconds
long tv_nsec; // Nanoseconds [0 .. 999999999]
};
@return 成功返回0,失败返回-1并设置错误码error
错误码error的类型:
EINTR 呼叫被信号处理程序中断;参见信号(7)。
EINVAL sem不是一个有效的信号量。
sem_trywait()可能会发生以下额外错误:
EAGAIN 如果没有阻塞(即信号量当前的值为零),则无法执行该操作。
sem_timedwait()可能会出现以下其他错误:
EINVAL abs_timeout.tv_nsecs的值小于0,或大于或等于10亿。
ETIMEDOUT
在锁定信号灯之前,调用超时。
*/
递增(解锁)信号量
#include <semaphore.h>
int sem_post(sem_t *sem);
/**
@brief 递增(解锁)sem指向的信号量。如果信号量的值因此变得大于零,那么在sem_wait(3)调用中阻塞的另一个进程或线程将被唤醒并继续锁定信号量
@param sem 指向被操作的信号量
@return 成功返回0,失败返回-1,并设置错误码error
错误码error类型:
EINVAL sem不是一个有效的信号量。
EOVERFLOW
超过信号量的最大允许值。
*/
获取信号量的当前值
#include <semaphore.h>
int sem_getvalue(sem_t *sem, int *sval);
/**
@brief 将指向sem的信号量的当前值放入sval指向的整数中。如果一个或多个进程或线程被阻塞,等待用sem_wait()锁定信号量,POSIX.1-2001允许sval中返回值的两种可能性:要么返回0;或者其绝对值是当前在sem_ wait()中被阻塞的进程和线程的数量的计数的负数。Linux采用了前一种行为。
@param sem 指向被操作的信号量
@param sval 存储信号量的当前值
@return 成功返回0,失败返回-1,并设置错误码error
错误码error类型:
EINVAL sem不是一个有效的信号量。
Trip:信号量的值可能在sem_getvalue()返回时已经更改。
*/
销毁信号量
#include <semaphore.h>
int sem_destroy(sem_t *sem);
/**
@brief 销毁sem指向的地址处的未命名信号量,只有已由sem_init()初始化的信号量才应使用sem_destroy()销毁。破坏其他进程或线程当前被阻塞的信号量(在sem_wait()中)会产生未定义的行为。使用已销毁的信号量会产生未定义的结果,直到使用sem_init()重新初始化信号量为止。
@param sem 指向被操作的信号量
@return 成功返回0,失败返回-1,并设置错误码error
错误码error类型:
EINVAL sem不是一个有效的信号量。
*/
无名信号量例程
/**
无名信号量的使用例程:该程序需要两个命令行参数。
第一个命令行参数argv[1]:指定一个秒值,用于设置报警计时器以生成SIGALRM信号。
此处理程序执行sem_post(3),以使用sem_timedwait()来增加main()中正在等待的信号量。
第二个命令行参数argv[2]:指定sem_timedwait()的超时长度(以秒为单位)。
程序在两次不同运行中发生的情况:
$ ./a.out 2 3
main() about to call sem_timedwait()
sem_post() from handler
sem_getvalue() from handler; value = 1
sem_timedwait() succeeded
$ ./a.out 2 1
main() about to call sem_timedwait()
sem_timedwait() timed out
Trip: 编译时需要加上动态库 -lrt
*/
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <semaphore.h>
#include <time.h>
#include <assert.h>
#include <errno.h>
#include <signal.h>
sem_t sem;
#define handle_error(msg) \
do { perror(msg); exit(EXIT_FAILURE); } while (0)
static void handler(int sig)
{
char msg1[] = "sem_post() from handler\n";
char msg2[] = "sem_post() failed\n";
write(STDOUT_FILENO, msg1, strlen(msg1));
if (sem_post(&sem) == -1)
{
write(STDERR_FILENO, msg2, strlen(msg2));
_exit(EXIT_FAILURE);
}
}
int main(int argc, char *argv[])
{
struct sigaction sa;
struct timespec ts;
int s;
if (argc != 3)
{
fprintf(stderr, "Usage: %s <alarm-secs> <wait-secs>\n", argv[0]);
exit(EXIT_FAILURE);
}
if (sem_init(&sem, 0, 0) == -1)
handle_error("sem_init");
// 建立SIGALRM处理程序;使用argv[1]设置报警计时器
sa.sa_handler = handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if (sigaction(SIGALRM, &sa, NULL) == -1)
handle_error("sigaction");
alarm(atoi(argv[1]));
// 将相对间隔计算为当前时间加上给定argv[2]的秒数
if (clock_gettime(CLOCK_REALTIME, &ts) == -1)
handle_error("clock_gettime");
ts.tv_sec += atoi(argv[2]);
printf("main() about to call sem_timedwait()\n");
while ((s = sem_timedwait(&sem, &ts)) == -1 && errno == EINTR)
continue; // 如果被处理程序中断,则重新启动
// 检查发生了什么
if (s == -1)
{
if (errno == ETIMEDOUT)
printf("sem_timedwait() timed out\n");
else
perror("sem_timedwait");
}
else
printf("sem_timedwait() succeeded\n");
exit((s == 0) ? EXIT_SUCCESS : EXIT_FAILURE);
}
/** 运行结果:
$ ./a.out 2 3
main() about to call sem_timedwait()
sem_post() from handler
sem_getvalue() from handler; value = 1
sem_timedwait() succeeded
$ ./a.out 2 1
main() about to call sem_timedwait()
sem_timedwait() timed out
*/
命名信号量相关库函数
编译时需要连接动态库 -lrt 或 -pthread
创建或打开一个命名信号量
#include <fcntl.h>
#include <sys/stat.h>
#include <semaphore.h>
sem_t *sem_open(const char *name, int oflag);
/**
@brief 创建一个新的POSIX信号量或打开一个现有的信号量。信号量由name标识。有关name构造的详细信息,请参见sem_overview(7)
@param name 信号量的名称
@param oflag 指定控制调用操作的标志(可以通过包含<fcntl.h>来获得标志值的定义。)如果在oflog中指定了O_CREAT,则在信号量不存在的情况下创建该信号量。信号量的所有者(用户ID)被设置为调用进程的有效用户ID。组所有权(组ID)设置为调用进程的有效组ID。如果在oflog中同时指定了O_CREAT和O_EXCL,那么如果具有给定名称的信号量已经存在,则返回错误。如果在oflog中指定了O_CREAT,则必须使用sem_open()的重载版本提供另外两个参数。
@return 成功返回新信号量的地址;这个地址在调用其他与信号量相关的函数时使用。失败返回SEM_FAILED,并设置错误码errno
错误码error类型:
EACCES 信号量存在,但调用方没有打开它的权限。
EEXIST 在oflog中同时指定了O_CREAT和O_EXCL,但已经存在具有此名称的信号量。
EINVAL 值大于SEM_value_MAX。
EINVAL name只包含“/”,后面没有其他字符。
EMFILE 进程已具有最大数量的文件并打开。
ENAMETOOLONG 名字太长了。
ENFILE 已达到打开文件总数的系统限制。
ENOENT oflog中没有指定O_CREAT标志,也不存在具有此名称的信号量;或者,指定了O_CREAT,但名称格式不正确。
ENOMEM 内存不足。
*/
sem_t *sem_open(const char *name, int oflag,
mode_t mode, unsigned int value);
/**
@brief 创建一个新的POSIX信号量或打开一个现有的信号量。信号量由name标识。有关name构造的详细信息,请参见sem_overview(7)
@param name 信号量的名称
@param oflag 指定控制调用操作的标志(可以通过包含<fcntl.h>来获得标志值的定义。)如果在oflog中指定了O_CREAT,则在信号量不存在的情况下创建该信号量。信号量的所有者(用户ID)被设置为调用进程的有效用户ID。组所有权(组ID)设置为调用进程的有效组ID。如果在oflog中同时指定了O_CREAT和O_EXCL,那么如果具有给定名称的信号量已经存在,则返回错误。如果在oflog中指定了O_CREAT,则必须提供另外两个参数。mode参数指定要放置在新信号量上的权限,就像open(2)一样。(权限位的符号定义可以通过包含<sys/stat.h>来获得。)权限设置是根据进程umask屏蔽的。读取和写入权限都应该授予将访问信号量的每类用户。value参数指定新信号量的初始值。如果指定了O_CREAT,并且具有给定名称的信号量已经存在,则模式和值将被忽略。
@param mode 指定要放置在新信号量上的权限,就像open(2)一样。(权限位的符号定义可以通过包含<sys/stat.h>来获得。)权限设置是根据进程umask屏蔽的。读取和写入权限都应该授予将访问信号量的每类用户。
@param value 指定新信号量的初始值。如果指定了O_CREAT,并且具有给定名称的信号量已经存在,则模式和值将被忽略。
@return 成功返回新信号量的地址;这个地址在调用其他与信号量相关的函数时使用。失败返回SEM_FAILED,并设置错误码errno
错误码error类型:
EACCES 信号量存在,但调用方没有打开它的权限。
EEXIST 在oflog中同时指定了O_CREAT和O_EXCL,但已经存在具有此名称的信号量。
EINVAL 值大于SEM_value_MAX。
EINVAL name只包含“/”,后面没有其他字符。
EMFILE 进程已具有最大数量的文件并打开。
ENAMETOOLONG 名字太长了。
ENFILE 已达到打开文件总数的系统限制。
ENOENT oflog中没有指定O_CREAT标志,也不存在具有此名称的信号量;或者,指定了O_CREAT,但名称格式不正确。
ENOMEM 内存不足。
*/
关闭命名信号量
#include <semaphore.h>
int sem_close(sem_t *sem);
/**
@brief 关闭sem引用的命名信号量,允许释放系统为该信号量的调用进程分配的任何资源。
@param name 信号量的名称
@param oflag 指定控制调用操作的标志(可以通过包含<fcntl.h>来获得标志值的定义。)如果在oflog中指定了O_CREAT,则在信号量不存在的情况下创建该信号量。信号量的所有者(用户ID)被设置为调用进程的有效用户ID。组所有权(组ID)设置为调用进程的有效组ID。如果在oflog中同时指定了O_CREAT和O_EXCL,那么如果具有给定名称的信号量已经存在,则返回错误。如果在oflog中指定了O_CREAT,则必须提供另外两个参数。mode参数指定要放置在新信号量上的权限,就像open(2)一样。(权限位的符号定义可以通过包含<sys/stat.h>来获得。)权限设置是根据进程umask屏蔽的。读取和写入权限都应该授予将访问信号量的每类用户。value参数指定新信号量的初始值。如果指定了O_CREAT,并且具有给定名称的信号量已经存在,则模式和值将被忽略。
@param mode 指定要放置在新信号量上的权限,就像open(2)一样。(权限位的符号定义可以通过包含<sys/stat.h>来获得。)权限设置是根据进程umask屏蔽的。读取和写入权限都应该授予将访问信号量的每类用户。
@param value 指定新信号量的初始值。如果指定了O_CREAT,并且具有给定名称的信号量已经存在,则模式和值将被忽略。
@return 成功返回0,失败返回-1,并设置错误码error
错误码error类型:
EINVAL sem不是一个有效的信号量。
Trip: 所有打开的命名信号量在进程终止或execve(2)时自动关闭。
*/
删除命名信号量
#include <semaphore.h>
int sem_unlink(const char *name);
/**
@brief 删除name引用的命名信号量。信号量名称将立即删除。一旦打开信号量的所有其他进程关闭信号量,信号量就会被销毁
@param name 信号量的名称
@return 成功返回0,失败返回-1,并设置错误码error
错误码error类型:
EACCES 调用方无权取消此信号量的链接。
ENAMETOOLONG 名字太长了。
ENOENT 没有具有给定名称的信号量。
*/
操作命名信号量
操作命名信号量的方法和操作无名信号量的方法一样。
#include <semaphore.h>
int sem_wait(sem_t *sem);
int sem_trywait(sem_t *sem);
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
int sem_post(sem_t *sem);
int sem_getvalue(sem_t *sem, int *sval);
命名信号量例程
/// 待补充
提示:先做内容框架梳理,后期进行完善补充!