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


目录
相关文章
|
16天前
|
安全 Java 数据处理
Python网络编程基础(Socket编程)多线程/多进程服务器编程
【4月更文挑战第11天】在网络编程中,随着客户端数量的增加,服务器的处理能力成为了一个重要的考量因素。为了处理多个客户端的并发请求,我们通常需要采用多线程或多进程的方式。在本章中,我们将探讨多线程/多进程服务器编程的概念,并通过一个多线程服务器的示例来演示其实现。
|
1月前
|
消息中间件 安全 Linux
线程同步与IPC:单进程多线程环境下的选择与权衡
线程同步与IPC:单进程多线程环境下的选择与权衡
58 0
|
3天前
|
Java API 调度
[Java并发基础]多进程编程
[Java并发基础]多进程编程
|
18天前
|
存储 Shell Perl
Perl 教程 之 Perl 进程管理 4
Perl教程介绍了进程管理,包括使用$$或$PROCESS_ID获取PID,%ENV存储环境变量,exit()退出子进程,fork()创建新进程。在父进程返回子进程PID,在子进程返回0。fork与exec配合执行命令。示例展示了父进程如何等待子进程结束。当子进程变为僵死状态时,父进程需使用wait或waitpid终止,或设置$SIG{CHLD}为&quot;IGNORE&quot;。
16 1
|
24天前
|
算法 Linux Shell
linux系统的进程管理
linux系统的进程管理
19 2
|
1月前
|
消息中间件 存储 网络协议
Linux IPC 进程间通讯方式的深入对比与分析和权衡
Linux IPC 进程间通讯方式的深入对比与分析和权衡
69 0
|
1月前
|
存储 算法 Linux
【Linux 系统标准 进程资源】Linux 创建一个最基本的进程所需的资源分析,以及线程资源与之的差异
【Linux 系统标准 进程资源】Linux 创建一个最基本的进程所需的资源分析,以及线程资源与之的差异
25 0
|
1月前
|
监控 Linux Shell
【Shell 命令集合 系统管理 】⭐Linux 显示系统中的进程信息 procinfo命令 使用指南
【Shell 命令集合 系统管理 】⭐Linux 显示系统中的进程信息 procinfo命令 使用指南
26 0
|
2月前
|
Linux 调度 数据库
Linux下的系统编程——线程同步(十三)
Linux下的系统编程——线程同步(十三)
52 0
Linux下的系统编程——线程同步(十三)
|
6月前
|
存储 Linux 调度
Linux系统编程 多线程基础
Linux系统编程 多线程基础
30 0