进程互斥锁--二值信号量

Posted 二进制人生

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了进程互斥锁--二值信号量相关的知识,希望对你有一定的参考价值。

最近在收集轮子或者造轮子,意在写一个嵌入式linux通用库,包含常用的各种数据结构和接口。已完成如下:







进程互斥锁实现--二值信号量(本文)


文字整理自网络,不是重点,代码才是精华,请仔细品味代码。

开题原因

线程间可以通过互斥锁来实现临界资源的互斥访问,那进程间通过什么方式来达到这一目的呢?

如果是面试,我是面试官的华一定会问你进程间同步和通信的东西。嵌入式里多进程的架构还是挺常见的,进程之间少不了同步和通信。

linux里的信号量给我们提供了进程间同步和互斥的手段,今天将其封装成接口纳入通用库里。

内容目录

信号量概念信号量简介一般信号量二值信号量信号量接口介绍1 创建信号量2 删除和初始化信号量3 改变信号量的值4 sembuf中sem_flg的设置问题5 陷阱提示项目级封装

信号量概念

信号量简介

在对于临界区资源管理的过程中,多个程序同时访问一个共享资源经常容易引发一系列问题:如死锁,结果不唯一等等。

在1965年,由荷兰科学家E.W.Dijkstra提出了一种新的进程同步工具,信号量及其PV操作。

对于信号量的定义是这样的:

让多个进程通过特殊变量展开交互,一个进程在某一个关键点上被迫停止执行直至接收到对应的特殊变量值,通过这一措施,任何复杂的进程交互要求均可得到满足,这种特殊的变量就是信号量。

信号量的种类分为以下2种:

一般信号量

设s为一个记录型数据结构,其中value为整型变量,系统初始化时为其赋值,PV操作的原语描述如下:

P(s):将信号量value值减1,若结果小于0,则执行P操作的进程被阻塞,若结果大于等于0,则执行P操作的进程将继续执行。

V(s):将信号量的值加1,若结果不大于0,则执行V操作的进程从信号量s有关的list所知队列中释放一个进程,使其转化为就绪态,自己则继续执行,若结果大于0,则执行V操作的进程继续执行。

二值信号量

设s为一个记录型数据结构,其中分量value仅能取值0或1,二值信号量的PV操作的原语描述和一般信号量相同,虽然二值信号量仅能取值0或1,但可以证明他有着与其他信号量相同的表达能力

上面那些定义总结起来可以这样说:通过使用信号量生成令牌来授权,在任一时刻只能有一个执行线程访问代码的临界区域。临界区域是指执行数据更新的代码需要独占式地执行。

而信号量就可以提供这样的一种访问机制,让一个临界区同一时间只有一个线程在访问它,也就是说信号量是用来调协进程对共享资源的访问的一种手段。

信号量接口介绍

在linux里信号量的实现由两个版本,POSIX和System V。这里我们只介绍System V。
所有函数共用头文件

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

1 创建信号量

int semget(key_t key,int nsems,int flags)
                                  //返回:成功返回信号集ID,出错返回-1

(1)第一个参数key是长整型(唯一非零),系统建立IPC通讯 ( 消息队列、 信号量和 共享内存) 时必须指定一个ID值。通常情况下,该id值通过ftok函数得到,由内核变成标识符,要想让两个进程看到同一个信号集,只需设置同一个key值即可以。

(2)第二个参数nsem指定信号量集中需要的信号量数目,它的值几乎总是1。

(3)第三个参数flag是一组标志,当想要当信号量不存在时创建一个新的信号量,可以将flag设置为IPC_CREAT与文件权限做按位或操作。

设置了IPC_CREAT标志后,即使给出的key是一个已有信号量的key,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。一般我们会还或上一个文件权限,比如0666。

2 删除和初始化信号量

int semctl(int semid, int semnum, int cmd, ...);

如有需要第四个参数一般设置为union semnu arg;定义如下

union semun

    int val;  //使用的值
    struct semid_ds *buf;  //IPC_STAT、IPC_SET 使用的缓存区
    unsigned short *arry;  //GETALL,、SETALL 使用的数组
    struct seminfo *__buf; // IPC_INFO(Linux特有) 使用的缓存区
};

(1)sem_id是由semget返回的信号量标识符

(2)semnum当前信号量集的哪一个信号量

(3)cmd通常是下面两个值中的其中一个
SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。
IPC_RMID:用于删除一个已经无需继续使用的信号量标识符,删除的话就不需要缺省参数,只需要三个参数即可。

3 改变信号量的值

int semop(int semid, struct sembuf *sops, size_t nops);

(1)nsops:进行操作信号量的个数,即sops结构变量的个数,需大于或等于1。最常见设置此值等于1,只完成对一个信号量的操作

(2)sembuf的定义如下:

struct sembuf{ 
    short sem_num;   //除非使用一组信号量,否则它为0 
    short sem_op;   //信号量在一次操作中需要改变的数据,通常是两个数,                                        
                    //一个是-1,即P(等待)操作, 
                    //一个是+1,即V(发送信号)操作。 
    short sem_flg; //通常为SEM_UNDO,使操作系统跟踪信号量, 
                  //并在进程没有释放该信号量而终止时,操作系统释放信号量 
}; 

4 sembuf中sem_flg的设置问题

通常设置为SEM_UNDO,使操作系统跟踪信号量,并在进程没有释放该信号量而终止时,操作系统释放信号量,例如在二元信号量中,你不释放该信号量而异常退出,就会导致别的进程一直申请不到信号量,而一直处于挂起状态。

5 陷阱提示

(1) 在调用semget(key_t key,int nsems,int flags)第一次创建信号量时务必调用int semop(int semid, struct sembuf *sops, size_t nops);设置信号量的值,否则其初始值为0,后面如果直接进行P操作会导致死锁。

(2) 所有进程退出时务必手动删除信号量。

项目级封装

下面实现了一个二值信号量,可以当做线程的互斥锁来使用,用于进程间临界资源的互斥访问。
semMutex.h头文件

#ifndef _SEM_MUTEX_H_
#define _SEM_MUTEX_H_

#ifndef _SEM_MUTEX_C_
#define FUN_SEM_MUTEX extern
#else
#define FUN_SEM_MUTEX
#endif

FUN_SEM_MUTEX int semInitint keyId );

FUN_SEM_MUTEX int semDestroyint keyId,int semid );

FUN_SEM_MUTEX void semLockint semid );

FUN_SEM_MUTEX void semUnLockint semid );

#endif

C文件semMutex.c

#ifndef _SEM_MUTEX_C_
#define _SEM_MUTEX_C_

#include <sys/sem.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include "semMutex.h"

void semLockint semid )
{
    struct sembuf sem;

    sem.sem_num = 0;//指定第0号信号量
    sem.sem_op = -1;
    sem.sem_flg = SEM_UNDO;
    semop(semid, &sem, 1);
}

void semUnLockint semid )
{
    struct sembuf sem;

    sem.sem_num = 0;//指定第0号信号量
    sem.sem_op = 1;
    sem.sem_flg = SEM_UNDO;//使操作系统跟踪信号量,并在进程没有释放该信号量而终止时,操作系统释放信号量,可以避免死锁
    semop(semid, &sem, 1);
}

int semInitint keyId )
{    
    int semid = -1;
    char filename[64];
    key_t key;
    int fd;
    int init = 0;

    sprintf(filename, "/tmp/.%04d.%s", keyId,"sem");
    if( access(filename,F_OK) == 0 )//文件存在说明信号量已经被创建
    {
        init = 1;
    }

    fd = open(filename, O_RDWR|O_CREAT|O_EXCL, 0x777);
    if(fd > 0){ 
        close(fd);
    }

    key = ftok(filename, keyId);
    semid = semget(key, 1, IPC_CREAT | 0660);//创建一个信号量数目为1的信号量集
    if(semid != -1 && init == 0)//第一次创建信号量的人需要对信号量进行初始化,不然信号量的值默认是0
    {
        if(semctl(semid, 0, GETVAL) != 1)
        {
            printf("semnum has not been set, key[0x%x]\n", keyId);
            semctl(semid, 0, SETVAL, 1);
        }
        else
        {
            printf("semnum has been set, semid:%d key[0x%x]\n",semid,keyId);
        }
    }
    else if(semid == -1)
    {
        printf("create sem failed, key[0x%x]\n", keyId);
    }
    else
    {
        printf("semid:%d key[0x%x] already create\n",semid,keyId);
    }
    return semid;
}

int semDestroyint keyId,int semid )
{
    char filename[64];
    sprintf(filename, "/tmp/.%04d.%s", keyId,"sem");
    if( semctl(semid, 0, IPC_RMID) < 0 )//删除失败不一定是失败,可能已经被人删除了,判断下
    {       
        if(access(filename,F_OK) < 0)//文件不存在说明已经被其他人销毁了
            return 0;
        else
        {
            printf("rm sem failed!\n");
            return -1;
        }
    }
    //删除文件
    unlink(filename);
    return 0;
}

int main()
{
    int semId = semInit(888);
    semLock(semId);
    printf("sleep 10s\n");
    sleep(10);
    semUnLock(semId);
    sleep(10)
    semDestroy(888,semId);
}

#endif


图 二进制人生公众号



以上是关于进程互斥锁--二值信号量的主要内容,如果未能解决你的问题,请参考以下文章

二值信号量和互斥锁到底有什么区别?

信号量,互斥锁,读写锁和条件变量的区别

信号量互斥锁和条件变量的区别

守护进程,模拟抢票例子,互斥锁,信号量,队列总结

LiteOS 互斥锁机制

多进程(了解),守护进程,互斥锁,信号量,进程Queue与线程queue