Linux进程间通信(IPC)教程 Linux信号量:讲解POSIX信号量在Linux系统进程间通信中的编程实践

简介: Linux进程间通信(IPC)教程 Linux信号量:讲解POSIX信号量在Linux系统进程间通信中的编程实践

POSIX信号量概述

POSIX信号量有两种

有名信号量和无名信号量,无名信号量也被称作基于内存的信号量。

有名信号量通过IPC名字进行进程间的同步,而无名信号量如果不是放在进程间的共享内存区中,只能用来进行线程同步。

有名信号量一般保存在/dev/shm/ 目录下,像文件一样存储在文件系统中。

信号量的工作原理

由于信号量只能进行两种操作等待和发送信号,即P(sv)和V(sv),他们的行为是这样的:

(1)P(sv):如果sv的值大于零,就给它减1;如果它的值为零,就挂起该进程的执行

(2)V(sv):如果有其他进程因等待sv而被挂起,就让它恢复运行,如果没有进程因等待sv而挂起,就给它加1.

在信号量进行PV操作时都为原子操作(因为它需要保护临界资源)

注:原子操作:单指令的操作称为原子的,单条指令的执行是不会被打断的


二元信号量

二元信号量(Binary Semaphore)是最简单的一种锁(互斥锁),它只用两种状态:占用与非占用。所以它的引用计数为1。

无名信号量

  • 对于每个信号量,必须保证sem_init()对其只初始化一次。 (因为资源分配了,如果再次初始化为原来的资源数会与实际资源数不一致)
  • 使用完每个信号量,必须sem_destroy(),如果还有线程因由于该信号量阻塞,而且已经销毁了该信号量,会有问题。
  • ios不支持创建无名信号量.
  • 无名信号量特性

无名信号量的持续性要根据信号量在内存中的位置:

  • 如果无名信号量是在单个进程内部的数据空间中,即信号量只能在进程内部的各个线程间共享,那么信号量是随进程的持续性,当进程终止时它也就消失了。
  • 如果无名信号量位于不同进程的共享内存区,因此只要该共享内存区仍然存在,该信号量就会一直存在。所以此时无名信号量是随内核的持续性

如果我们想要在一个单一进程内使用POSIX信号量,那么使用无名信号量会更加简单。无名信号量只是创建和销毁有所改变,其他完全和有名信号量一样。

信号量的使用

(1)测试控制该资源的信号量

(2)信号量的值为正,进程获得该资源的使用权,进程将信号量减1,表示它使用了一个资源单位

(3)若此时信号量的值为0,则进程进入挂起状态(进程状态改变),直到信号量的值大于0,若进程被唤醒则返回至第一步。

注:信号量通过同步与互斥保证访问资源的一致性。


POSIX信号量的相关操作

  • 创建一个信号量

创建的过程还要求初始化信号量的值,根据信号量取值(代表可用资源的数目)的不同,信号量还可以分为:

  • 二值信号量:信号量的值只有0和1,这和互斥量很类型,若资源被锁住,信号量的值为0,若资源可用,则信号量的值为1;
  • 计数信号量:信号量的值在0到一个大于1的限制值(POSIX指出系统的最大限制值至少要为32767)。该计数表示可用的资源的个数。

注意:POSIX信号量并没有区分信号量类型。

使用二值信号量还是计数信号量,取决于我们如果对信号进行初始化和使用。

如果信号量值只能取0和1,那么它就是一个二值信号量。

当一个二值信号量值为1,我们则说它未加锁;若它的值为0,则说它已加锁。

  • 等待一个信号量(wait)

该操作会检查信号量的值,如果其值小于或等于0,那就阻塞,直到该值变成大于0,然后等待进程将信号量的值减1,进程获得共享资源的访问权限。

这整个操作必须是一个原子操作。该操作还经常被称为P操作(荷兰语Proberen,意为:尝试)。

  • 挂出一个信号量(post)

该操作将信号量的值加1,如果有进程阻塞着等待该信号量,那么其中一个进程将被唤醒。

该操作也必须是一个原子操作。该操作还经常被称为V操作(荷兰语Verhogen,意为:增加)


POSIX信号量的接口使用

  • 创建有名信号量
sem_t *sem_open(const char *name, int oflag, ... /* mode_t mode, unsigned int value */ );
//如果使用一个现存的有名信号量,我们只需指定两个参数:信号量名和oflag(oflag取0)。

说明:

把oflag设置为O_CREAT标志时,如果指定的信号量不存在则新建一个有名信号量;

如果指定的信号量已经存在,那么打开使用,无其他额外操作发生。

参数:

name: 信号量标识。

oflag:可以为:0,O_CREAT,O_EXCL

如果为0表示打开一个已存在的信号量,

如果为O_CREAT,表示如果信号量不存在就创建一个有名信号量,如果存在则打开被返回使用。此时mode和value需要指定。

如果为O_CREAT | O_EXCL,表示如果信号量已存在会返回错误。

mode:用来指定谁可以访问该信号量。它可以取打开文件时所用的权限位的取值。参考:stat结构

value:指定信号量的初始值。它可取值为:0-SEM_VALUE_MAX。

注意:

这里的name不能写成/tmp/aaa.sem这样的格式,因为在linux下,sem都是创建在/dev/shm目录下。

你可以将name写成“/mysem”或“mysem”,创建出来的文件都是“/dev/shm/sem.mysem”,千万不要写路径。也千万不要写“/tmp/mysem”之类的。

返回值:

    若成功,返回指向信号量的指针

    若失败,返回SEM_FAILED

  • 销毁有名信号量
int sem_unlink(const char *name);
//移除信号量的名字。如果当前没有打开的对该信号量的引用,那么就销毁它。否则,销毁被推迟到最后一个打开的引用被关闭。

参数:

name: 信号量的名字

返回值:

    若成功,返回0

    若失败,返回-1

  • 创建无名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

参数:

sem:声明的sem_t类型的变量,并把它的地址传给sem_init,以便对该变量进行初始化。

如果我们要在两个进程之间使用该无名信号量,我们需要确保sem参数指向这两个进程共享的内存范围内。

pshared: 指示我们是否要在多进程之间使用该无名信号量。如果要在多个进程之间使用,则将pshared设置为非0值。

value: 指定信号量的初始值。

返回值:

    若成功,返回0

    若失败,返回-1

  • 销毁无名信号量
int sem_destroy(sem_t *sem);
//调用sem_destroy后我们将不能再以sem为参数调用任何信号量函数,除非我们再次使用sem_init对sem进行初始化。

返回值:

    若成功,返回0

    若失败,返回-1

  • 关闭有名信号量
int sem_close(sem_t *sem);

说明:

关闭一个信号量并没有将他从系统中删除。POSIX 有名信号量是随内核持续的:即使当前没有进程打开着某个信号量,他的值仍保持。

返回值:

    若成功,返回0

    若失败,返回-1

  • 获取信号量的值
int sem_getvalue(sem_t *sem, int *restrict valp);
//

说明:

如果sem_getvalue执行成功,信号量的值将存入valp指向的整型变量中。

但是,需要小心,我们刚读出来的信号量值可能会改变(因为我们随时可能会使用该信号量值)。如果不采取额外的同步机制的话,sem_getvalue函数仅仅用来调试。

返回值:

    若成功,返回0

    若失败,返回-1

  • 对信号量值加1
int sem_post(sem_t *sem);
//类似于对一个二值信号量解锁或释放一个与计数信号量有关的资源。

说明:

当我们调用sem_post的时,如果此时有因为调用sem_waitsem_timedwait而阻塞的进程,

那么该进程将被唤醒,并且刚刚被sem_post加1的信号量计数紧接着又被sem_waitsem_timedwait减1。

返回值:

    若成功,返回0

    若失败,返回-1

  • 请求一个信号量(对信号量值执行减1操作)
int sem_wait(sem_t *sem);
//如果信号量计数为0,调用函数,将会阻塞。直到成功对信号量计数减1或被一个信号中断,sem_wait函数才会返回。
int sem_trywait(sem_t *sem);
//避免阻塞。调用函数时,如果信号量计数为0,sem_trywait会返回-1,并将errno设置为EAGAIN。

参数:

sem: sem_open函数返回的信号量指针

返回值:

    若成功,返回0

    若失败,返回-1

  • 计时等待(阻塞一段有限的时间)
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);

参数:

tsptr: 指定了希望等待的绝对时间。

如果信号量可以被立即减1,那么超时也无所谓,即使你指定了一个已经过去的时间,试图对信号量减1的操作也会成功。

如果直到超时,还不能对信号量计数减1,那么sem_timedwait函数将会返回-1,并将errno设置为ETIMEDOUT。

返回值:

    若成功,返回0

    若失败,返回-1

sem_timedwait等待计时底层是通过CLOCK_REALTIME时钟实现的,但是这有一个很大的问题.

当使用CLOCK_REALTIME作为超时参考时,如果系统时间发生了更改,那么计时器的行为可能会受到非常大的影响。例如:

  • 如果系统时间向前调整,可能导致计时器等待的时间大大超过预期,甚至可能无限等待。
  • 如果系统时间向后调整,计时器可能会立即超时,即使实际的等待时间远远不足。

这确实是CLOCK_REALTIME的一个明显缺陷,特别是在那些系统时间可能会被修改的环境中。例如,当系统与NTP服务器同步时,或者当管理员手动更改系统时间时。

信号量(semaphores)和条件变量(condition variables)都是同步原语,但它们的设计目的和使用场景有所不同。这也影响了它们在不同时间源上的实现选择。

  1. 设计哲学和目的
  • 信号量主要用于控制对资源的访问。它们是计数器,允许多个线程同时访问一个资源,或者确保资源的互斥访问。
  • 条件变量则用于线程之间的通信,允许一个线程等待特定的条件成立,由另一个线程来通知。
  1. 历史和遗留问题
  • sem_timedwait最初被引入时,CLOCK_MONOTONIC还没有广泛被接受或使用。随着时间的推移,尽管CLOCK_MONOTONIC变得更加普及,但修改已有的系统调用来支持新的时间源可能会引入向后兼容性问题。
  1. 实现复杂性
  • 为信号量添加对CLOCK_MONOTONIC的支持意味着需要更改内部的数据结构和逻辑。这可能会增加实现的复杂性,并可能引入新的错误。
  1. 需求
  • 可能的一个原因是,对于许多使用信号量的应用程序,使用CLOCK_REALTIME已经足够了。而对于需要更精确和稳定的计时需求,条件变量可能更为常见。
  1. 可选方案
  • 对于需要CLOCK_MONOTONIC的信号量,程序员可以选择其他同步原语或使用其他方法(例如,结合clock_nanosleep)来实现所需的行为。

总的来说,尽管从技术上说,为信号量添加CLOCK_MONOTONIC的支持是可行的,但由于历史、设计和需求的原因,这种支持并没有被加入到标准库中。然而,随着操作系统和库的持续发展,未来的版本中可能会考虑这种支持。

信号量(semaphores)和条件变量(condition variables)都是同步原语,但它们的设计目的和使用场景有所不同。这也影响了它们在不同时间源上的实现选择。

  1. 设计哲学和目的
  • 信号量主要用于控制对资源的访问。它们是计数器,允许多个线程同时访问一个资源,或者确保资源的互斥访问。
  • 条件变量则用于线程之间的通信,允许一个线程等待特定的条件成立,由另一个线程来通知。
  1. 历史和遗留问题
  • sem_timedwait最初被引入时,CLOCK_MONOTONIC还没有广泛被接受或使用。随着时间的推移,尽管CLOCK_MONOTONIC变得更加普及,但修改已有的系统调用来支持新的时间源可能会引入向后兼容性问题。
  1. 实现复杂性
  • 为信号量添加对CLOCK_MONOTONIC的支持意味着需要更改内部的数据结构和逻辑。这可能会增加实现的复杂性,并可能引入新的错误。
  1. 需求
  • 可能的一个原因是,对于许多使用信号量的应用程序,使用CLOCK_REALTIME已经足够了。而对于需要更精确和稳定的计时需求,条件变量可能更为常见。
  1. 可选方案
  • 对于需要CLOCK_MONOTONIC的信号量,程序员可以选择其他同步原语或使用其他方法(例如,结合clock_nanosleep)来实现所需的行为。

总的来说,尽管从技术上说,为信号量添加CLOCK_MONOTONIC的支持是可行的,但由于历史、设计和需求的原因,这种支持并没有被加入到标准库中。然而,随着操作系统和库的持续发展,未来的版本中可能会考虑这种支持。


POSIX信号量的部分使用示例(代码段)

  • 创建信号量(代码段)
sem_t *sem;
 
//Create posix semaphore
int create_mysem(sem_t **sem,char * sem_name)
{
   *sem = sem_open(sem_name, O_CREAT|O_RDWR, 0644, 0); //The initial value of the semaphore is 0
   if(*sem == SEM_FAILED)
   {
        perror("sem_open");
        return -1 ;
   }
   else
    printf("create %s success\n",sem_name);
    
    return 0;
}
  • 打开信号量(代码段)
sem_t *sem;
 
//The semaphore is used to communicate with the xxx
int open_mysem(char * sem_name)
{
    sf_sem = sem_open(sem_name, 0);
    if(sf_sem == SEM_FAILED)
    {
          perror("sem_open");
          return -1 ;
    }
    else
    {
      sf_flag=1;
      printf("%s open success \n",sem_name);
    }
    return 0;
}
  • 计时等待案例
 
#include <time.h>
#include <semaphore.h>
#include <sys/signal.h>
#include <stdio.h>
pthread_mutex_t timelock;   //用于控制系统时间不被随意修改
sem_t Heartbeat_sem;            
static void init_env(void);
int main(void)
{
    int ret=-1;
    for(;;)
    {
        pthread_mutex_lock(&timelock);
        struct timespec ts;
        clock_gettime(CLOCK_REALTIME, &ts); //获取当前时间
        ts.tv_sec += 1;         //现在ts为1秒后的时间
        ret = sem_timedwait(&Heartbeat_sem, &ts);
        pthread_mutex_unlock(&timelock);
        if(ret==0)
        {
          //deal 
        }
        else if (ret == -1 && errno == ETIMEDOUT)
        {
                printf("timeout\n");
                continue;
        }
        else
        {
                perror("bad time");
                //异常
                continue;
        }
    }    
 return 0;
}
void init_env(void)
{
 
    if(sem_init(&Heartbeat_sem, 0, 0)<0)
    {
       perror("faid to sem");
       exit(-1);
    }
    if(pthread_mutex_init(&timelock,NULL)!=0)
    {
       perror("faid to mutex");
       exit(-1);
    }
}


目录
相关文章
|
1月前
|
资源调度 Linux 调度
Linux c/c++之进程基础
这篇文章主要介绍了Linux下C/C++进程的基本概念、组成、模式、运行和状态,以及如何使用系统调用创建和管理进程。
31 0
|
8天前
|
缓存 监控 Linux
linux进程管理万字详解!!!
本文档介绍了Linux系统中进程管理、系统负载监控、内存监控和磁盘监控的基本概念和常用命令。主要内容包括: 1. **进程管理**: - **进程介绍**:程序与进程的关系、进程的生命周期、查看进程号和父进程号的方法。 - **进程监控命令**:`ps`、`pstree`、`pidof`、`top`、`htop`、`lsof`等命令的使用方法和案例。 - **进程管理命令**:控制信号、`kill`、`pkill`、`killall`、前台和后台运行、`screen`、`nohup`等命令的使用方法和案例。
33 4
linux进程管理万字详解!!!
|
8天前
|
算法 Linux 定位技术
Linux内核中的进程调度算法解析####
【10月更文挑战第29天】 本文深入剖析了Linux操作系统的心脏——内核中至关重要的组成部分之一,即进程调度机制。不同于传统的摘要概述,我们将通过一段引人入胜的故事线来揭开进程调度算法的神秘面纱,展现其背后的精妙设计与复杂逻辑,让读者仿佛跟随一位虚拟的“进程侦探”,一步步探索Linux如何高效、公平地管理众多进程,确保系统资源的最优分配与利用。 ####
33 4
|
9天前
|
缓存 负载均衡 算法
Linux内核中的进程调度算法解析####
本文深入探讨了Linux操作系统核心组件之一——进程调度器,着重分析了其采用的CFS(完全公平调度器)算法。不同于传统摘要对研究背景、方法、结果和结论的概述,本文摘要将直接揭示CFS算法的核心优势及其在现代多核处理器环境下如何实现高效、公平的资源分配,同时简要提及该算法如何优化系统响应时间和吞吐量,为读者快速构建对Linux进程调度机制的认知框架。 ####
|
10天前
|
消息中间件 存储 Linux
|
16天前
|
运维 Linux
Linux查找占用的端口,并杀死进程的简单方法
通过上述步骤和命令,您能够迅速识别并根据实际情况管理Linux系统中占用特定端口的进程。为了获得更全面的服务器管理技巧和解决方案,提供了丰富的资源和专业服务,是您提升运维技能的理想选择。
16 1
|
28天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
【10月更文挑战第9天】本文将深入浅出地介绍Linux系统中的进程管理机制,包括进程的概念、状态、调度以及如何在Linux环境下进行进程控制。我们将通过直观的语言和生动的比喻,让读者轻松掌握这一核心概念。文章不仅适合初学者构建基础,也能帮助有经验的用户加深对进程管理的理解。
19 1
|
1月前
|
消息中间件 Linux API
Linux c/c++之IPC进程间通信
这篇文章详细介绍了Linux下C/C++进程间通信(IPC)的三种主要技术:共享内存、消息队列和信号量,包括它们的编程模型、API函数原型、优势与缺点,并通过示例代码展示了它们的创建、使用和管理方法。
27 0
Linux c/c++之IPC进程间通信
|
1月前
|
Linux C++
Linux c/c++进程间通信(1)
这篇文章介绍了Linux下C/C++进程间通信的几种方式,包括普通文件、文件映射虚拟内存、管道通信(FIFO),并提供了示例代码和标准输入输出设备的应用。
22 0
Linux c/c++进程间通信(1)
|
1月前
|
Linux C++
Linux c/c++之进程的创建
这篇文章介绍了在Linux环境下使用C/C++创建进程的三种方式:system函数、fork函数以及exec族函数,并展示了它们的代码示例和运行结果。
31 0
Linux c/c++之进程的创建