Linux C语言 38-进程间通信IPC之信号量

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);
命名信号量例程
/// 待补充

提示:先做内容框架梳理,后期进行完善补充!

  • 8
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值