Linux 信号量

Linux 信号量

  • 一、信号量的基本概念
    • 1. 计数信号量(Counting Semaphore)
    • 2. 二进制信号量(Binary Semaphore)
  • 二 、使用场景
    • 1. 信号量需要用到的库
      • 系统V IPC头文件
    • 2. 代码演示
      • 1. 头文件和结构体定义
      • 2. 主函数
      • 3. 创建/获取共享内存
      • 4. 连接共享内存到当前进程的地址空间
      • 5. 创建、初始化二元信号量
      • 6. 加锁和访问共享内存
      • 7. 解锁和清理
      • 8. 全部代码

一、信号量的基本概念

信号量(Semaphore)是操作系统中用于管理并发进程的一种同步机制。它们用于控制对共享资源的访问,以避免竞争条件和数据不一致的问题。信号量主要分为两种类型:计数信号量(Counting Semaphore)和二进制信号量(Binary Semaphore),又称互斥量(Mutex)。

1. 计数信号量(Counting Semaphore)

计数信号量是一个整数值,可以用于控制对多个资源的访问。它的初始值表示可用资源的数量。计数信号量支持两种基本操作:

  • P(wait)操作:试图将信号量的值减1。如果信号量的值大于0,则操作成功;如果信号量的值为0,则进程会阻塞,直到信号量的值大于0。
  • V(signal)操作:将信号量的值加1。如果有进程因信号量的值为0而阻塞,则会唤醒其中的一个进程。

2. 二进制信号量(Binary Semaphore)

二进制信号量只有0和1两个值,常用于实现互斥访问。它也支持P和V操作,但由于值只能为0或1,因此操作更简单:

  • P(wait)操作:如果信号量的值为1,则将其置为0,并允许进程进入临界区;如果信号量的值为0,则进程会阻塞,直到信号量的值为1。
  • V(signal)操作:将信号量的值置为1,并唤醒因等待信号量而阻塞的进程。

二 、使用场景

  • 进程同步:确保多个进程在访问共享资源时不会发生冲突。
  • 资源计数:例如,限制同时访问某个资源的线程或进程数量。
  • 生产者-消费者问题:用来协调生产者和消费者之间的生产和消费速度。

1. 信号量需要用到的库

系统V IPC头文件

  1. #include

    提供了System V IPC(进程间通信)机制的通用接口,包括消息队列、信号量和共享内存。
    主要用于生成IPC键值(ftok 函数)和定义IPC相关的常量。

  2. #include

    提供了System V共享内存的接口,包括创建、连接、分离和控制共享内存段的函数。
    常用函数包括 shmgetshmatshmdtshmctl 等。

  3. #include

    定义了许多数据类型,用于其他系统调用接口。例如,pid_t(进程ID)、key_t(IPC键值)、size_t(对象大小)等。
    提供了一些POSIX标准定义的类型,用于与系统调用进行交互。

  4. #include

    提供了System V信号量的接口,包括创建、操作和控制信号量集的函数。
    常用函数包括 semgetsemopsemctl 等。

2. 代码演示

以下例程来自b站公开课,C++中高级程序员实战(码农联盟)

这段代码演示了如何使用信号量对共享内存进行加锁,以确保对共享内存的访问是线程安全的。下面是对这段代码的详细分析

1. 头文件和结构体定义

这里头文件里存放了以下函数声明,csemp 类的声明和定义,用于信号量操作。

#include "_public.h"

struct stgirl     // 超女结构体。
{
  int  no;        // 编号。
  char name[51];  // 姓名,注意,不能用string。
};

  • 包含必要的头文件 _public.h。
  • 定义一个结构体 stgirl,包含两个成员:no(编号)和 name(姓名)。注意,这里不能使用C++的 std::string 类型,而是使用了C风格的字符数组。

2. 主函数

int main(int argc, char *argv[])
{
  if (argc != 3) { 
    cout << "Using:./demo no name\n"; 
    return -1; 
  }

  • 检查命令行参数是否正确(应包含两个参数:编号和姓名)。如果参数数量不正确,输出使用方法并返回错误。

3. 创建/获取共享内存

相关的如 shmget() 函数的介绍放在我另外一个笔记中:Linux 共享内存

  int shmid = shmget(0x5005, sizeof(stgirl), 0640 | IPC_CREAT);
  if (shmid == -1) {
    cout << "shmget(0x5005) failed.\n";
    return -1;
  }

  cout << "shmid=" << shmid << endl;

  • 使用 shmget 创建或获取一个共享内存段,键值为 0x5005,大小为 stgirl 结构体的大小,权限为 0640。如果不存在则创建。
  • 如果 shmget 失败,输出错误信息并返回。

补充

组合使用权限标志

  • 在上述示例中,IPC_CREAT 常常与权限标志组合使用,如 0644 或 0666。这些权限标志类似于文件权限,定义了对 IPC 资源的读写权限。
  • 0644:所有者读写权限,其他用户只读权限。
  • 0666:所有用户都具有读写权限。

0640 权限表示:

  • 所有者(Owner):读(r)+ 写(w),即6。
  • 所属组(Group):读(r),即4。
  • 其他用户(Others):没有权限(0)。
  • 这个权限设置保证了对IPC资源的合理访问控制,防止未授权用户对资源进行读写操作。

4. 连接共享内存到当前进程的地址空间

  stgirl *ptr = (stgirl *)shmat(shmid, 0, 0);
  if (ptr == (void *)-1) {
    cout << "shmat() failed\n";
    return -1;
  }

  • 使用 shmat 将共享内存连接到当前进程的地址空间。
  • 如果 shmat 失败,输出错误信息并返回-1。( shmat 返回 (void *)-1,则表示连接失败。)

5. 创建、初始化二元信号量

  csemp mutex;
  if (mutex.init(0x5005) == false) {
    cout << "mutex.init(0x5005) failed.\n";
    return -1;
  }
  • 创建一个信号量 mutex。
  • 初始化信号量,使用与共享内存相同的键值 0x5005。
  • 如果初始化失败,输出错误信息并返回。

6. 加锁和访问共享内存

  cout << "申请加锁...\n";
  mutex.wait(); // 申请加锁。
  cout << "申请加锁成功。\n";

  cout << "原值:no=" << ptr->no << ",name=" << ptr->name << endl;  // 显示共享内存中的原值。
  ptr->no = atoi(argv[1]);        // 对超女结构体的no成员赋值。
  strcpy(ptr->name, argv[2]);    // 对超女结构体的name成员赋值。
  cout << "新值:no=" << ptr->no << ",name=" << ptr->name << endl;  // 显示共享内存中的当前值。
  sleep(10);

  • 输出加锁申请信息,并调用 mutex.wait() 进行加锁。
  • 显示共享内存中的原值。
  • 更新 stgirl 结构体的 no 和 name 成员为命令行参数提供的值。
  • 显示更新后的值。
  • 休眠10秒,模拟处理过程。

其中 wait() 是 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。

// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;      // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;   // P操作的value必须小于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }

  return true;
}

atoi 函数
atoi(ASCII to Integer)函数是C标准库中的一个函数,用于将C风格的字符串转换为整数。它的声明在头文件 中。
在 main 函数中,命令行参数会被存储在 argv 数组中:

argv[0] 存储程序的名称,即 ./demo。
argv[1] 存储第一个参数,即 “12345”。
argv[2] 存储第二个参数,即 “John”。
atoi(argv[1]); 的作用是将字符串 “12345” 转换为整数 12345。

在这段代码中,atoi(argv[1]) 的具体作用如下:

  • 将命令行参数 argv[1](即字符串 “12345”)转换为整数 12345。
  • 然后,将该整数赋值给结构体 stgirl 的成员 no:ptr->no = atoi(argv[1]);。

7. 解锁和清理

  mutex.post(); // 解锁。
  cout << "解锁。\n";

  // 查看信号量  :ipcs -s    // 删除信号量  :ipcrm sem 信号量id
  // 查看共享内存:ipcs -m    // 删除共享内存:ipcrm -m  共享内存id

  // 第4步:把共享内存从当前进程中分离。
  shmdt(ptr);

  // 第5步:删除共享内存。
  if (shmctl(shmid, IPC_RMID, 0) == -1) {
    cout << "shmctl failed\n";
    return -1;
  }
}

  • 调用 mutex.post() 进行解锁。
  • 输出解锁信息。
  • 提示用户如何查看和删除信号量及共享内存。
  • 使用 shmdt 将共享内存从当前进程中分离。
  • 删除共享内存。

mutex.post() 表示对信号量进行V操作,解锁对共享资源的访问。具体来说,当一个进程完成对共享资源的操作后,它会调用 mutex.post() 来释放信号量,使得其他等待该信号量的进程可以继续执行。
mutex是我们之前用类创建的对象,它去调用post()函数

post函数

// 信号量的V操作(把信号量的值减value)。
bool csemp::post(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;     // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;  // V操作的value必须大于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }

  return true;
}

8. 全部代码

_public.h

#ifndef __PUBLIC_HH
#define __PUBLIC_HH 1

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
using namespace std;


// 信号量。
class csemp
{
private:
  union semun  // 用于信号量操作的共同体。  操作信号量需要这样的数据结构
  {
    int val;
    struct semid_ds *buf;
    unsigned short  *arry;
  };

  int   m_semid;         // 信号量id(描述符)。

  // 如果把sem_flg设置为SEM_UNDO,操作系统将跟踪进程对信号量的修改情况,
  // 在全部修改过信号量的进程(正常或异常)终止后,操作系统将把信号量恢复为初始值。
  // 如果信号量用于互斥锁,设置为SEM_UNDO。
  // 如果信号量用于生产消费者模型,设置为0。
  short m_sem_flg;

  csemp(const csemp &) = delete;             // 禁用拷贝构造函数。
  csemp &operator=(const csemp &) = delete;  // 禁用赋值函数。
public:
  csemp():m_semid(-1){}
  // 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
  // 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
  // 如果用于生产消费者模型,value填0,sem_flg填0。
  bool init(key_t key,unsigned short value=1,short sem_flg=SEM_UNDO);
  bool wait(short value=-1);// 信号量的P操作,如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
  bool post(short value=1); // 信号量的V操作。
  int  getvalue();           // 获取信号量的值,成功返回信号量的值,失败返回-1。
  bool destroy();            // 销毁信号量。
 ~csemp();
};

#endif

_public.cpp

#include "_public.h"

// 如果信号量已存在,获取信号量;如果信号量不存在,则创建它并初始化为value。
// 如果用于互斥锁,value填1,sem_flg填SEM_UNDO。
// 如果用于生产消费者模型,value填0,sem_flg填0。
bool csemp::init(key_t key,unsigned short value,short sem_flg)
{
  if (m_semid!=-1) return false; // 如果已经初始化了,不必再次初始化。

  m_sem_flg=sem_flg;

  // 信号量的初始化不能直接用semget(key,1,0666|IPC_CREAT)
  // 因为信号量创建后,初始值是0,如果用于互斥锁,需要把它的初始值设置为1,
  // 而获取信号量则不需要设置初始值,所以,创建信号量和获取信号量的流程不同。

  // 信号量的初始化分三个步骤:
  // 1)获取信号量,如果成功,函数返回。
  // 2)如果失败,则创建信号量。
  // 3) 设置信号量的初始值。

  // 获取信号量。
  if ( (m_semid=semget(key,1,0666)) == -1)
  {
    // 如果信号量不存在,创建它。
    if (errno==ENOENT)
    {
      // 用IPC_EXCL标志确保只有一个进程创建并初始化信号量,其它进程只能获取。
      if ( (m_semid=semget(key,1,0666|IPC_CREAT|IPC_EXCL)) == -1)
      {
        if (errno==EEXIST) // 如果错误代码是信号量已存在,则再次获取信号量。
        {
          if ( (m_semid=semget(key,1,0666)) == -1)
          { 
            perror("init 1 semget()"); return false; 
          }
          return true;
        }
        else  // 如果是其它错误,返回失败。
        {
          perror("init 2 semget()"); return false;
        }
      }

      // 信号量创建成功后,还需要把它初始化成value。
      union semun sem_union;
      sem_union.val = value;   // 设置信号量的初始值。
      if (semctl(m_semid,0,SETVAL,sem_union) <  0) 
      { 
        perror("init semctl()"); return false; 
      }
    }
    else
    { perror("init 3 semget()"); return false; }
  }

  return true;
}

// 信号量的P操作(把信号量的值减value),如果信号量的值是0,将阻塞等待,直到信号量的值大于0。
bool csemp::wait(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;      // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;   // P操作的value必须小于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("p semop()"); return false; }

  return true;
}

// 信号量的V操作(把信号量的值减value)。
bool csemp::post(short value)
{
  if (m_semid==-1) return false;

  struct sembuf sem_b;
  sem_b.sem_num = 0;     // 信号量编号,0代表第一个信号量。
  sem_b.sem_op = value;  // V操作的value必须大于0。
  sem_b.sem_flg = m_sem_flg;
  if (semop(m_semid,&sem_b,1) == -1) { perror("V semop()"); return false; }

  return true;
}

// 获取信号量的值,成功返回信号量的值,失败返回-1。
int csemp::getvalue()
{
  return semctl(m_semid,0,GETVAL);
}

// 销毁信号量。
bool csemp::destroy()
{
  if (m_semid==-1) return false;

  if (semctl(m_semid,0,IPC_RMID) == -1) { perror("destroy semctl()"); return false; }

  return true;
}

csemp::~csemp()
{
}

demo3.cpp

// demo3.cpp,本程序演示用信号量给共享内存加锁。
#include "_public.h"

struct stgirl     // 超女结构体。
{
  int  no;        // 编号。
  char name[51];  // 姓名,注意,不能用string。
};

int main(int argc,char *argv[])
{
  if (argc!=3) { cout << "Using:./demo no name\n"; return -1; }

  // 第1步:创建/获取共享内存,键值key为0x5005,也可以用其它的值。
  int shmid=shmget(0x5005, sizeof(stgirl), 0640|IPC_CREAT);
  if ( shmid ==-1 )
  { 
    cout << "shmget(0x5005) failed.\n"; return -1; 
  }

  cout << "shmid=" << shmid << endl;

  // 第2步:把共享内存连接到当前进程的地址空间。
  stgirl *ptr=(stgirl *)shmat(shmid,0,0);
  if ( ptr==(void *)-1 )
  { 
    cout << "shmat() failed\n"; return -1; 
  }

  // 创建、初始化二元信号量。
  csemp mutex;
  if (mutex.init(0x5005)==false)
  {
    cout << "mutex.init(0x5005) failed.\n"; return -1;
  }

  cout << "申请加锁...\n";
  mutex.wait(); // 申请加锁。
  cout << "申请加锁成功。\n";

  // 第3步:使用共享内存,对共享内存进行读/写。
  cout << "原值:no=" << ptr->no << ",name=" << ptr->name << endl;  // 显示共享内存中的原值。
  ptr->no=atoi(argv[1]);        // 对超女结构体的no成员赋值。
  strcpy(ptr->name,argv[2]);    // 对超女结构体的name成员赋值。
  cout << "新值:no=" << ptr->no << ",name=" << ptr->name << endl;  // 显示共享内存中的当前值。
  sleep(10);

  mutex.post(); // 解锁。
  cout << "解锁。\n";

  // 查看信号量  :ipcs -s    // 删除信号量  :ipcrm sem 信号量id
  // 查看共享内存:ipcs -m    // 删除共享内存:ipcrm -m  共享内存id

  // 第4步:把共享内存从当前进程中分离。
  shmdt(ptr);

  // 第5步:删除共享内存。
  if (shmctl(shmid,IPC_RMID,0)==-1)
  { 
    cout << "shmctl failed\n"; return -1; 
  }
}

你可能感兴趣的:(Linux编程基础,linux,网络,服务器)