Linux系统编程系列之POSIX信号量

一、什么是POSIX信号量

        POSIX信号量是一种用于线程之间同步和互斥的机制,它是由POSIX标准定义的一种IPC(进程间通信)机制,可以用于进程间或线程间的同步操作。POSIX信号量分成两种,POSIX匿名信号量和POSIX具体信号量。跟管道有点像,有匿名管道和具名管道。

二、特性

        1、POSIX匿名信号量

                (1)、通常用于线程

                (2)、只存在于内存,在文件系统中不可见

        2、POSIX具名信号量

                (1)、通常用在进程

                (2)、存在于文件系统 /dev/shm 中,可被不同进程操作

   三、POSIX信号量使用步骤

        1、POSIX匿名信号量

                (1)、使用sem_init(),初始化匿名信号量

                (2)、使用sem_wait()和sem_post()等,进行 P/V 操作

                (3)、使用sem_destroy(),销毁匿名信号量

        2、POSIX具名信号量

                (1)、使用sem_open(),创建或打开具名信号量、

                (2)、使用sem_wait()和sem_post()等,进行 P/V 操作

                (3)、使用sem_close(),关闭具名信号量

                (4)、使用sem_unlink(),彻底删除不再使用的信号量(可选)

四、相关的函数API接口

        1、定义

// POSIX信号量是一种特性的变量
// 声明一个POSIX信号量s
sem_t s;

        2、初始化和销毁匿名信号量

// 初始化匿名信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);

// 接口说明
        返回值:成功返回0,失败返回-1
        参数sem:待初始化信号量指针
        参数pshared:执行信号的作用范围
            (1)0,作用于进程内的线程间
            (2)非0,作用于进程间

        参数value:信号量的初始值

// 销毁匿名信号量
int sem_destroy(sem_t *sem);

         3、创建和打开具名信号量

// 打开
sem_t *sem_open(const char *name, int oflag);

// 接口说明
        返回值:成功返回信号量的地址,失败返回一个宏SEM_FAILED
        参数name:具名信号量的路径
        参数oflag:与文件打开的参数一样

// 创建
sem_t *sem_open(const char *name, 
                int oflag,
                mode_t mode, unsigned int value);


// 接口说明
        返回值:成功返回信号量的地址,失败返回一个宏SEM_FAILED
        参数name:具名信号量的路径
        参数oflag:与文件打开的参数一样
        参数value:具名信号量初始值

         4、P/V操作

// P操作,阻塞等待申请资源
int sem_wait(sem_t *sem);

// P操作,非阻塞等待申请资源
int sem_trywait(sem_t *sem);

// P操作,在一定时间内进行阻塞等待
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);

// V操作,释放资源
int sem_post(sem_t *sem);

         5、关闭具名信号量

        POSIX具名信号量跟文件操作非常类似,打开之后会在内核需要对其维护,因此在不再需要的时候应该关闭

// 关闭具名信号量
 int sem_close(sem_t *sem);

        6、删除具名信号量

        即使所有进程都关闭了信号量并且退出,具名信号量对应的文件是不会消失的,并且会保留所有 P/V 操作的值,如果不再需要这个文件本身,除了可以直接在文件系统中删除外,还可以使用以下接口删除

// 删除具名信号量
int sem_unlink(const char *name);

五、案例

        1、使用POSIX匿名信号量实现线程间数据发送和接收,一条线程发送,一条线程接收

// POSIX匿名信号量的案例

#include 
#include 
#include 
#include 
#include 


char data[100];

sem_t data_sem; // 声明一个POSIX匿名信号量data_sem

pthread_once_t data_sem_once_init;  // 函数单例变量,用来指定只初始化一次
pthread_once_t data_sem_once_destroy;   // 函数单例变量,用来指定只销毁一次

// 初始化匿名信号量data_sem 
void data_sem_init(void)
{
    // 定义data_sem, 指定用于线程间,初始值0,用来实现同步
    int ret = sem_init(&data_sem, 0, 0);
    if(ret == -1)
    {
        perror("sem_init fail");
    }
}

// 销毁匿名信号量data_sem 
void data_sem_destroy(void)
{
    int ret = sem_destroy(&data_sem);
    if(ret == -1)
    {
        perror("sem_destroy fail");
    }
}


// 线程1的例程函数,用来接收数据
void *recv_routine(void *arg)
{
    printf("I am recv_routine, my tid = %ld\n", pthread_self());

    // 设置线程分离 
    pthread_detach(pthread_self()); 

    // 函数单例,本程序之后执行一次
    pthread_once(&data_sem_once_init, data_sem_init);

    while(1)
    {
        // P操作,相当于给线程1发送信号
        printf("wait data...\n");
        sem_wait(&data_sem);

        printf("pthread1 read data: %s\n", data);
        memset(data, 0, sizeof(data));
    }

    // 函数单例,本程序之后执行一次
    pthread_once(&data_sem_once_destroy, data_sem_destroy);
}

// 线程2的例程函数,用来发送数据
void *send_routine(void *arg)
{
    printf("I am send_routine, my tid = %ld\n", pthread_self());

    // 设置线程分离 
    pthread_detach(pthread_self()); 

    // 函数单例,本程序之后执行一次
    pthread_once(&data_sem_once_init, data_sem_init);

    while(1)
    {
        printf("please input data:\n");
        fgets(data, 100, stdin);
        

        // V操作,相当于给线程1发送信号
        sem_post(&data_sem);
        printf("pthread2 send data\n");
    }

    // 函数单例,本程序之后执行一次
    pthread_once(&data_sem_once_destroy, data_sem_destroy);
}

int main(int argc, char *argv[])
{
    pthread_t tid1, tid2;

    // 创建线程1,用来接收数据
    errno = pthread_create(&tid1, NULL, recv_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create recv_routine success, tid = %ld\n", tid1);
    }
    else
    {
        perror("pthread create recv_routine fail\n");
    }

  
    errno = pthread_create(&tid2, NULL, send_routine, NULL);
    if(errno == 0)
    {
        printf("pthread create send_routine success, tid = %ld\n", tid2);
    }
    else
    {
        perror("pthread create send_routine fail\n");
    }

    // 一定要加这个,否则主函数直接退出,相当于进程退出,所有线程也退出
    // 或者加上while(1)等让主函数不退出
    pthread_exit(0);
    
    return 0;
}

Linux系统编程系列之POSIX信号量_第1张图片

        2、使用具名POSIX结合共享内存的方式,实现进程间互相收发数据

// POSIX具名信号量的案例

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include            
#include        


#define A 1
//#define B 1  // 编译第二版本时,请去掉前面的注释,同时注释A的宏定义

// 注意A进程的P信号量与B进程的V信号量相对应,所以要修改信号量序号的下标
#if A
#define DATA_P_NAME  "sem1"
#define DATA_V_NAME  "sem2"
#define SPACE_P_NAME "sem3"
#define SPACE_V_NAME "sem4"

#elif B
#define DATA_P_NAME  "sem2"
#define DATA_V_NAME  "sem1"
#define SPACE_P_NAME "sem4"
#define SPACE_V_NAME "sem3"
#endif


#define SHM_KEY 0x01
#define SHM_SIZE 4096
// #define SEM_NAME "data_sem2"


int sem_id = -1;
// 映射的虚拟地址
char *shm_addr = NULL;

// 共享内存初始化
int shm_init(void)
{
    // 1、获取KEY值
    key_t shm_key = ftok("./", SHM_KEY);
    if(shm_key == -1)
    {
        perror("ftok fail");
        return -1;
    }

    // 2、指定共享内存,获取共享内存对象ID
    int shm_id = shmget(shm_key, SHM_SIZE, IPC_CREAT | 0666);
    if(shm_id == -1)
    {
        perror("shmget fail");
        return -1;
    }

    // 3、映射共享内存
    shm_addr = (char*)shmat(shm_id, NULL, 0);
    if(shm_addr == (void*)-1)
    {
        perror("shmat fail");
        return -1;
    }
}

// 具名信号量初始化
sem_t *my_sem_init(const char *name, int value)
{
    // 尝试打开具名信号量,并初始化为1
    sem_t *sem = sem_open(name, O_EXCL| O_CREAT, 0666, value);
    // 如果具名信号量存在就不需要初始化,直接打开就行
    if(sem == SEM_FAILED && errno == EEXIST)
    {
        sem = sem_open(name, O_CREAT);
        if(sem == SEM_FAILED)
        {
            perror("sem_open1 fail");
            return NULL;
        }
    }
    else if(sem == SEM_FAILED)
    {
        perror("sem_open2 fail");
        return NULL;   
    }

    return sem;
}    

int main(int argc, char *argv[])
{
    int ret = 0;

    ret = shm_init();
    if(ret == -1)
    {
        return -1;
    }
 
    sem_t *Data_P = my_sem_init(DATA_P_NAME, 0);
    sem_t *Data_V = my_sem_init(DATA_V_NAME, 0);
    sem_t *Space_P = my_sem_init(SPACE_P_NAME, 1);
    sem_t *Space_V = my_sem_init(SPACE_V_NAME, 1);
    
    
    pid_t pid = fork();
    // 父进程负责发送数据
    if(pid > 0)
    {
        while(1)
        {
            // 申请空间,P操作
            printf("wait Space_P...\n");
            sem_wait(Space_P);
            printf("get Space_P\n");

            printf("please input data: \n");
            fgets(shm_addr, SHM_SIZE, stdin);

            // 释放数据,V操作
            sem_post(Data_V);
            printf("set Data_V, send data success\n");
        }
    }
    // 子进程负责接收数据
    else if(pid == 0)
    {
        while(1)
        {
           // 申请数据,P操作
            printf("wait Data_P...\n");
            sem_wait(Data_P);

            printf("read Data: %s", shm_addr);
            memset(shm_addr, 0, SHM_SIZE);

            // 释放空间,V操作
            sem_post(Space_V);
            printf("set Space_V\n");
        }
    }
    else
    {
        perror("fork fail");
        return -1;
    }

    return 0;
}

Linux系统编程系列之POSIX信号量_第2张图片

Linux系统编程系列之POSIX信号量_第3张图片

        注意:编译时编译两个版本,第一个版本直接编译,另外一个版本需要展开B的宏定义后再编译,跟前几篇的信号量组的案例一样。这个案例有点bug,每次需要重新把/dev/shm/下四个具名信号量都删除了才能重新运行中,目前努力修复中。

六、总结

        POSIX信号量是一种用于线程之间同步和互斥的机制,当然也可以用于进程间通信。POSIX信号量有匿名信号量和具名信号量,跟管道有点像,两种信号量的使用步骤需要遵循一定的步骤,使用方法不一样。

你可能感兴趣的:(C语言程序设计,Linux,c语言,linux)