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);
    }
}


目录
相关文章
|
11天前
|
算法 Linux 调度
深入理解Linux操作系统的进程管理
本文旨在探讨Linux操作系统中的进程管理机制,包括进程的创建、执行、调度和终止等环节。通过对Linux内核中相关模块的分析,揭示其高效的进程管理策略,为开发者提供优化程序性能和资源利用率的参考。
33 1
|
6天前
|
SQL 运维 监控
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
南大通用GBase 8a MPP Cluster Linux端SQL进程监控工具
|
12天前
|
监控 算法 Linux
Linux内核锁机制深度剖析与实践优化####
本文作为一篇技术性文章,深入探讨了Linux操作系统内核中锁机制的工作原理、类型及其在并发控制中的应用,旨在为开发者提供关于如何有效利用这些工具来提升系统性能和稳定性的见解。不同于常规摘要的概述性质,本文将直接通过具体案例分析,展示在不同场景下选择合适的锁策略对于解决竞争条件、死锁问题的重要性,以及如何根据实际需求调整锁的粒度以达到最佳效果,为读者呈现一份实用性强的实践指南。 ####
|
12天前
|
缓存 监控 网络协议
Linux操作系统的内核优化与实践####
本文旨在探讨Linux操作系统内核的优化策略与实际应用案例,深入分析内核参数调优、编译选项配置及实时性能监控的方法。通过具体实例讲解如何根据不同应用场景调整内核设置,以提升系统性能和稳定性,为系统管理员和技术爱好者提供实用的优化指南。 ####
|
14天前
|
运维 监控 Linux
Linux操作系统的守护进程与服务管理深度剖析####
本文作为一篇技术性文章,旨在深入探讨Linux操作系统中守护进程与服务管理的机制、工具及实践策略。不同于传统的摘要概述,本文将以“守护进程的生命周期”为核心线索,串联起Linux服务管理的各个方面,从守护进程的定义与特性出发,逐步深入到Systemd的工作原理、服务单元文件编写、服务状态管理以及故障排查技巧,为读者呈现一幅Linux服务管理的全景图。 ####
|
6月前
|
监控 Linux 应用服务中间件
探索Linux中的`ps`命令:进程监控与分析的利器
探索Linux中的`ps`命令:进程监控与分析的利器
136 13
|
5月前
|
运维 关系型数据库 MySQL
掌握taskset:优化你的Linux进程,提升系统性能
在多核处理器成为现代计算标准的今天,运维人员和性能调优人员面临着如何有效利用这些处理能力的挑战。优化进程运行的位置不仅可以提高性能,还能更好地管理和分配系统资源。 其中,taskset命令是一个强大的工具,它允许管理员将进程绑定到特定的CPU核心,减少上下文切换的开销,从而提升整体效率。
掌握taskset:优化你的Linux进程,提升系统性能
|
5月前
|
弹性计算 Linux 区块链
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
186 4
Linux系统CPU异常占用(minerd 、tplink等挖矿进程)
|
4月前
|
算法 Linux 调度
探索进程调度:Linux内核中的完全公平调度器
【8月更文挑战第2天】在操作系统的心脏——内核中,进程调度算法扮演着至关重要的角色。本文将深入探讨Linux内核中的完全公平调度器(Completely Fair Scheduler, CFS),一个旨在提供公平时间分配给所有进程的调度器。我们将通过代码示例,理解CFS如何管理运行队列、选择下一个运行进程以及如何对实时负载进行响应。文章将揭示CFS的设计哲学,并展示其如何在现代多任务计算环境中实现高效的资源分配。
|
5月前
|
存储 缓存 安全
【Linux】冯诺依曼体系结构与操作系统及其进程
【Linux】冯诺依曼体系结构与操作系统及其进程
174 1