首页 > 系统相关 >Linux进程间的通信方式(三)System V 信号量

Linux进程间的通信方式(三)System V 信号量

时间:2024-07-05 09:26:18浏览次数:18  
标签:semid int System number 信号量 Linux 操作 sem

文章目录


前言

本文主要探讨 linux 下进程间的通信方式之信号量,最后通过一个简单的示例实现一个父子进程之间操作临界资源使用信号量处理同步与互斥的功能。


一、信号量概念

信号量(Semaphore)的概念最早由荷兰计算机科学家 Dijkstra(迪杰斯特拉)提出,有时又称“信号灯”。信号量本质上是一个计数器,用于协调多个进程(包括但不限于父子进程)对共享数据对象的读写。与其它进程间通信方式不大相同,它不以传送数据为目的,主要是作为一种进程间的同步机制用来保护共享资源。

1.1 信号跟信号量的区别

信号是一种处理异步事件的机制,用于通知进程某个事件的发生。而信号量是一种用于处理进程或者线程间同步与互斥的机制。

1.2 同步跟互斥的区别

同步与互斥机制是计算机系统中用于控制进程或线程对某些特定资源访问的两种机制。

1.2.1 同步的概念

同步指的是多个进程或线程在相互配合完成某项任务时,需要按照某种特定的顺序或条件来执行。也就是说,一个进程或线程的执行可能依赖于另一个进程或线程的执行结果或状态。

1.2.2 互斥的概念

互斥是指某个资源在某一时刻只允许一个进程或线程访问以防止多个进程或线程同时访问共享资源造成的数据不一致或其他问题。

1.3 原子操作概念

原子操作是指一个操作在执行过程中不会被其他线程或任务打断或分割的一个操作。即这个操作要么完全执行成功,要么完全不执行,不会出现部分执行或中间状态。这种特性使得原子操作在多线程或并发执行的情况下,能够保证对共享资源的操作能够正确、完整地进行。

信号量的原子操作特性是实现进程间同步与互斥的关键。通过P操作和V操作来申请和释放信号量资源,信号量的原子操作特性可以确保在任一时刻只有一个进程能够访问共享资源,从而实现了对临界资源的安全访问。

二、信号量的相关操作函数

2.1 ftok 函数(获取一个key值)

函数原型key_t ftok(const char *pathname, int proj_id);
功能基于文件路径和proj_id子序号生成一个键值
参数
pathname: 文件路径
proj_id: 子序号
返回值成功返回一个键值,失败返回-1并设置errno指明错误的原因

2.2 semget函数(创建或者获取信号量)

函数原型int semget(key_t key, int nsems, int semflg);
功能创建或者获取一个信号量集标识符
参数
key: 键值,唯一标识一个信号量集,可取由ftok创建的key值或指定的一个非负整数值
nsems: 指定信号量集中信号量的个数。
semflg: 这是一组标志,用于控制 semget 函数的行为
返回值成功返回一个信号量集标识符,失败返回-1并设置errno指明错误的原因

这里涉及到一个概念,信号量集。什么是信号量集呢?你可以把它理解为一个数组,这个数组里存放一堆信号量,称之为信号量集。
参数 nsems 就是指定信号量集中信号量的个数。我们使用信号量一般的目的就只是用来做进程间的同步,所以我们只需要设置 nsems 为1即可,后续只对这个信号量进行P、V操作。

关于semflg我已经在第一章介绍消息队列的时候就详细讲过了,这里就不再讲了,可以参考一下msgget函数介绍那段。

传送门:Linux进程间的通信方式(一)System V 消息队列

2.3 semctl函数(控制信号量)

函数原型int semctl(int semid, int semnum, int cmd, ...);
功能信号量控制操作,包括创建、删除、获取和设置信号量的初始值以及获取信号量集的信息
参数
semid: 信号量集的标识符,通过 semget 函数获取
semnum: 指定信号量的编号,用于指定在信号量集中的特定信号量。简单理解就是下标
cmd: 指定信号量的操作类型。常用的就两个,IPC_RMID 和 SETVAL
... : 可选的参数,根据 cmd 的不同,传递不同类型的数据。通常是 union semun
返回值成功返回0,失败返回-1并设置errno指明错误的原因

我们先来看看man手册的这段描述

semctl() performs the control operation specified by cmd on the System V semaphore set identified by semid, or on the semnum-th semaphore of that set. (The semaphores in a set are numbered starting at 0.)

翻译过来就是 semctl 函数对一个由 semid 标识的System V信号量集或是该信号量集中第 semnum 个信号量执行 cmd 指定的控制操作指令(一个信号量集中的信号量从0开始编号)

在这里要注意-th的用法,意思就是第几个的意思。

简而言之就是说 semctl 函数既可以对整个信号量集做控制操作,也可以对单个信号量做控制操作。对单个信号量做控制操作的时候可以通过下标指定操作的信号量,要注意的是第一个信号量的下标为 0,这个跟数组是一样的。

结合上面我们对 semget 函数的分析,一般 semctl 函数的用法是这样的

/* 创建或者获取一个信号量集 */
int create_sem(const char *path, int proj_id, int nsems) 
{
    key_t keyval = ftok(path, proj_id);

    int semid = semget(keyval, nsems, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1) {
        if (errno == EEXIST) {
            semid = semget(keyval, 0, 0);
            printf("the semaphore set whose key is %d and semid is %d is already exist!\n", keyval, semid);
            return semid;
        } else 
            perror("semget");
    }

    return semid;
}

/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
    union semun su;
    su.val = value;
    if (semctl(semid, semnum, SETVAL, su) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

int main() 
{
    int ret, semid;

    // 创建或者获取一个信号量集
    semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 1);
    if (semid < 0)
        return -1;

    // 初始化信号量值为1
    init_sem(semid, 0, 1);  

	return 0;
}

1. 创建或者获取一个信号量集并获取该信号量集标识符
在这里我们首先创建了一个信号量集并设置信号量的个数为1。
注意:当我们尝试去获取一个已经存在的信号量集的时候,semget 函数的第二第三个参数是被忽略的,因为你可以直接填0。

2. 初始化该信号量集(操作整个信号量集或对指定信号量进行初始值设置)
接着我们设置信号量集中下标为0也就是第一个信号量的初始值为1。
至此就完成了信号量的初始化了,接下来只需要对该信号量进行P、V操作即可。

常用cmd操作指令

指令描述
IPC_RMID删除信号量集
SETVAL设置指定信号量的值

当cmd为IPC_RMID时只需要三个参数

/* 删除信号量集 */
int del_sem(int semid) {
    if (semctl(semid, 0, IPC_RMID) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

当cmd为SETVAL时需要四个参数,第四个参数是一个联合体,定义如下

union semun {
               int              val;    /* Value for SETVAL */
               struct semid_ds *buf;    /* Buffer for IPC_STAT, IPC_SET */
               unsigned short  *array;  /* Array for GETALL, SETALL */
               struct seminfo  *__buf;  /* Buffer for IPC_INFO
                                           (Linux-specific) */
};

/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
    union semun su;
    su.val = value;
    if (semctl(semid, semnum, SETVAL, su) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

2.4 semop函数(操作信号量)

函数原型int semop(int semid, struct sembuf *sops, size_t nsops);
功能用于对信号量集或信号量的值进行P和V操作
参数
semid: 信号量集合的标识符,通过 semget 函数获取
sops: 一个指向sembuf结构体数组的指针
nsops: 该结构体数组的元素个数
返回值成功返回0,失败返回-1并设置errno指明错误的原因

关于指针sops可以这样理解。首先它是一个sembuf类型的结构体指针,它指向一个sembuf类型的结构体数组。每个结构体数组元素是一个信号量,一整个结构体数组就是一整个信号量集,这样通过sops可以操作信号量集中的信号量了。

sembuf结构体的定义如下

struct sembuf {
    unsigned short sem_num;  // 信号量集合中的信号量编号
    short sem_op;            // 操作类型
    short sem_flg;           // 操作标志
};

以下是结构体sembuf中各个成员变量的解析:

  1. sem_num: 信号量集中某个特定信号量的编号,第一个信号量的编号是0。

  2. sem_op: 要执行的操作,一般只会用到两个值-1和1,PV操作均为原子操作。

    P(-1)操作: 尝试获取资源。若获取失败则阻塞等待,若获取成功则将信号量的值减1表示信号量被本进程占用,其他进程不可获取。
    V(+1)操作: 释放资源。若操作完临界资源后需要释放信号量让其他进程抢夺则进行V操作将信号量的值加1表示信号量被释放,其他进程可以抢夺该信号量。

  3. sem_flg: 信号操作标志,一般有如下两种

    IPC_NOWAIT: 对信号的操作不能满足时,semop()不会阻塞并立即返回,同时设定错误信息。
    SEM_UNDO: 程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。这样做的目的在于避免程序在异常情况下结束时未将锁定的资源解锁造成该资源永远锁定。

    一般我们将它设置为SEM_UNDO

具体使用方法:

/* P 操作(-1)阻塞获取一个信号量 */
int sem_p(int semid, int semnum) {
    struct sembuf sb = {semnum, -1, SEM_UNDO};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

/* V 操作(+1)释放一个信号量 */
int sem_v(int semid, int semnum) {
    struct sembuf sb = {semnum, 1, SEM_UNDO};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

 sem_p(semid, 0);  // P操作,进入临界区
 sem_v(semid, 0);  // V操作,离开临界区

三、信号量应用示例

3.1 System V信号量应用示例代码

头文件:sem_com.h

#ifndef SEM_COM_H
#define SEM_COM_H

#ifdef __cplusplus
extern "C" {
#endif

#include <sys/types.h>
#include <sys/sem.h>
#include <sys/ipc.h>

#define SEM_PATH_NAME "/tmp"
#define SEM_PROJ_ID 0x6666

// 错误处理宏
#define ERR_EXIT(m) \
    do { \
        perror(m); \
        exit(EXIT_FAILURE); \
    } while (0)

union semun {
    int val;
    struct semid_ds *buf;
    unsigned short *array;
    struct seminfo *__buf;
};

int create_sem(const char *path, int proj_id, int nsems);
int init_sem(int semid, int semnum, int value);
int del_sem(int semid);
int sem_p(int semid, int semnum);
int sem_v(int semid, int semnum);

#ifdef __cplusplus
}
#endif

#endif // !SEM_COM_H

实现文件:sem_com.c

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>

#include "sem_com.h"

/* 创建或者获取一个信号量集 */
int create_sem(const char *path, int proj_id, int nsems) 
{
    key_t keyval = ftok(path, proj_id);

    int semid = semget(keyval, nsems, IPC_CREAT | IPC_EXCL | 0666);
    if (semid == -1) {
        if (errno == EEXIST) {
            semid = semget(keyval, 0, 0);
            printf("the semaphore set whose key is %d and semid is %d is already exist!\n", keyval, semid);
            return semid;
        } else 
            perror("semget");
    }

    return semid;
}

/* 初始化信号量集 */
int init_sem(int semid, int semnum, int value) {
    union semun su;
    su.val = value;
    if (semctl(semid, semnum, SETVAL, su) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

/* 删除信号量集 */
int del_sem(int semid) {
    if (semctl(semid, 0, IPC_RMID) == -1) {
        ERR_EXIT("semctl");
    }
    return 0;
}

/* P 操作(-1)阻塞获取一个信号量 */
int sem_p(int semid, int semnum) {
    struct sembuf sb = {semnum, -1, SEM_UNDO};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

/* V 操作(+1)释放一个信号量 */
int sem_v(int semid, int semnum) {
    struct sembuf sb = {semnum, 1, SEM_UNDO};
    if (semop(semid, &sb, 1) == -1) {
        ERR_EXIT("semop");
    }
    return 0;
}

主程序文件:example.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
 #include <signal.h>
 #include <sys/wait.h>
#include "sem_com.h"

static int g_number = 0;

int main() 
{
    int ret, semid;

    // 创建或者获取一个信号量集
    semid = create_sem(SEM_PATH_NAME, SEM_PROJ_ID, 1);
    if (semid < 0)
        return -1;

    // 初始化信号量值为1
    init_sem(semid, 0, 1);  

    pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        return -1;
    } else if (pid == 0) { /* child */
        printf("I am child,pid:%d,ppid:%d\n", getpid(), getppid());

        for (int i = 0; i < 15; i++) {
            sem_p(semid, 0);  // P操作,进入临界区
            g_number += i;
            printf("child: g_number: %d\n", g_number);
            sem_v(semid, 0);  // V操作,离开临界区
            sleep(1);
        }

        exit(0);

    } else {
        printf("I am father,pid:%d,ppid:%d\n", getpid(), getppid());

        for (int i = 0; i < 10; i++) {
            sem_p(semid, 0);  // P操作,进入临界区
            g_number -= i;
            printf("father: g_number: %d\n", g_number);
            sem_v(semid, 0);  // V操作,离开临界区
            sleep(1);
        }

        /*阻塞等待子进程结束*/
        ret = wait(NULL);

        if (ret == -1) {
            perror("wait");
            return -1;
        }

        del_sem(semid); // 删除信号量
    }
    
    return 0;
}

Makefile文件

# Makefile for compiling example program with semaphore implementation

# Compiler
CC = gcc

# Compiler flags
CFLAGS = -Wall -g

# Target executable
TARGET = example

# Source files
SRCS = example.c sem_com.c

# Object files
OBJS = $(SRCS:.c=.o)

# Header files
HEADERS = sem_com.h

# Default target
all: $(TARGET)

# Linking
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $(TARGET) $(OBJS)

# Compiling source files into object files
%.o: %.c $(HEADERS)
	$(CC) $(CFLAGS) -c $< -o $@

# Clean up
clean:
	rm -f $(OBJS) $(TARGET)

# Phony targets
.PHONY: all clean

3.2 应用示例代码讲解

编译后输出如下

jeff@jeff:~/jeffPro/practice/IPC/semaphore/sem1$ make
gcc -Wall -g -c example.c -o example.o
gcc -Wall -g -c sem_com.c -o sem_com.o
gcc -Wall -g -o example example.o sem_com.o
jeff@jeff:~/jeffPro/practice/IPC/semaphore/sem1$ ./example
I am father,pid:6338,ppid:2334
father: g_number: 0
I am child,pid:6339,ppid:6338
child: g_number: 0
child: g_number: 1
father: g_number: -1
child: g_number: 3
father: g_number: -3
child: g_number: 6
father: g_number: -6
child: g_number: 10
father: g_number: -10
child: g_number: 15
father: g_number: -15
father: g_number: -21
child: g_number: 21
father: g_number: -28
child: g_number: 28
father: g_number: -36
child: g_number: 36
father: g_number: -45
child: g_number: 45
child: g_number: 55
child: g_number: 66
child: g_number: 78
child: g_number: 91
child: g_number: 105
jeff@jeff:~/jeffPro/practice/IPC/semaphore/sem1$

通过 ipcs -s 指令可以查看到创建的信号量相关信息

jeff@jeff:~$ ipcs -s

------ Semaphore Arrays --------
key        semid      owner      perms      nsems
0x66000002 27         jeff       666        1

可以看到通过使用信号量实现了父子进程之间的同步与互斥功能。

标签:semid,int,System,number,信号量,Linux,操作,sem
From: https://blog.csdn.net/Mr_Jaychong/article/details/140083578

相关文章

  • Linux C系列学习笔记_第四集
    局部变量和全局变量可以重名,作用域遵循就近原则#include<stdio.h>inta=10;//全局变量avoidfun(){printf("1.%d\n",a);inta=1;//a=1;printf("2.%d\n",a);}intmain(){inta=8;......
  • 常见Linux命令
    1、查看目录:ls常用用法:ls-l:以列表的形式展示;简写ll效果展示:2、终端清屏:clear常用用法:ctr+L:清空屏幕当前的内容,不会重置终端效果展示:使用前使用后3、切换目录:cd常用用法:cd/:切换到根目录cd/xx(目录名):切换到根目录下的xx目录cd..:切换到上一级目录c......
  • Linux常用命令快速回顾
    目录帮助命令软件安装工作目录查看、切换pwd(printworkdirectory)打印当前工作目录cd(changediretory)tree[目录名]——以树状图列出文件目录结构文件/目录创建、删除、查找、查看mkdir创建目录touch 创建文件rm删除文件find[路径]-name'*.py'查找文件......
  • 【Linux】多线程(互斥 && 同步)
    我们在上一节多线程提到没有任何保护措施的抢票是会造成数据不一致的问题的。那我们怎么办?答案就是进行加锁。目录加锁:认识锁和接口:初始化:加锁&&解锁:全局的方式:局部的方式:原理角度理解:实现角度理解:同步:加锁:认识锁和接口:初始化:这个就是我们互斥锁的类型。......
  • 同步、异步、阻塞、非阻塞、Linux五种 I/O 模型,一篇文章搞定
    ● 什么是同步、什么是异步?什么是阻塞、什么非阻塞?我自己的理解,大白话啊,同步和异步指的是函数调用完成任务的程度。一个任务的完成,包括发起、执行和结果返回三个阶段。 同步(synchronize)调用涵盖了这三个阶段。调用结束之后,任务肯定是有结果的,无论成败。 ......
  • linux找回root密码
      linux找回root密码如果您忘记了Linux系统的root密码,并且没有其他方法可以访问系统,您可以按照以下步骤重置root密码:重启您的Linux系统。在启动时,当GRUB菜单出现时,选择要启动的内核版本,然后按下 e 键来编辑启动选项。 找到以 linux 开头的行,通常......
  • Linux磁盘管理
    磁盘管理【1】、Linux常用的分区格式在新增磁盘后要对其进行分区,分区后才可以去存储数据MBR分区格式:比较古老的分区格式,只能划分4个主分区,如果四个分区划分完成后,硬盘空间还有剩余,那剩余的分区也不能够使用。后来新增加扩展分区(容器)功能,可在扩展分区内划分出更多的逻辑分区......
  • Java SSH 客户端 如何删除 linux 机器上的 文件
    在Java中,可以使用SSH客户端库如JSch(JavaSecureChannel)来连接到远程Linux机器并执行命令,包括删除文件。下面是一个使用JSch库的示例,展示如何通过SSH删除远程Linux机器上的文件。步骤:添加JSch库:确保您的项目包含JSch库。如果您使用的是Maven,可以在pom.xml......
  • 欧拉 EulerOS是华为基于CentOS源代码,面向企业应用环境开发的一个商用Linux发行版。
    欧拉EulerOS是华为基于CentOS源代码,面向企业应用环境开发的一个商用Linux发行版。EulerOSEulerOS是华为基于CentOS源代码,面向企业应用环境开发的一个商用Linux发行版。EulerOS开发者华为技术有限公司作業系統家族Unix ,Linux,CentOS運作狀態活跃源码模式开源软件当前版本2.......
  • Linux网络服务面试题
    1、查看一个网络接口的方法有哪些?  ①查看目录/etc/sysconfig/network-scripts/下的网卡对应的配置文件ifcfg-ens33  ②ifconfigens332、如何给一个网络接口设置多个IP地址?  临时配置:ifconfigens33:1IP地址netmask掩码 up  永久配置:在网卡配置文件目录......