Linux·信号量全解

目录

信号量

进程间

【无名信号量完成 有血缘关系的进程间 互斥】

知识点2【有名信号量 没有血缘进程互斥】

1、创建一个有名信号量

2、信号量的关闭:

3、信号量文件的删除

4、P操作 sem_wait V操作sem_post 销毁信号量sem_destroy

知识点3【有名信号量 没有血缘进程同步】

线程间

什么是临界资源?

访问临界资源应遵循如下原则:

什么是同步互斥?

1、同步

2、互斥

什么是PV操作?

信号量的特性

内核间

概念

应用场景

使用方法

内核信号量的构成

信号量的API

PV操作

获取信号量(P)

释放内核信号量(V)

补充

内核信号量的使用例程

读-写信号量


信号量

信号量(semaphore)有时被称为信号灯,是操作系统用来解决并发中的互斥和同步问题的一种方法

进程间

【无名信号量完成 有血缘关系的进程间 互斥】

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <semaphore.h>
#include <unistd.h>
#include <sys/mman.h>
#include <semaphore.h>

void myPrintf(char *str){
  while(*str != 0){
    printf("%c", *str);
    fflush(stdout);
    str++;
    sleep(1);
  }
	printf("\n");
}

int main(int argc, char const *argv[])
{
  //互斥 只需要 定义一个信号量
  sem_t *sem;
  //匿名映射mmap MAP_ANONYMOUS -1不用打开文件
  sem = mmap(NULL, sizeof(sem_t), PROT_READ|PROT_WRITE, MAP_SHARED|MAP_ANONYMOUS, -1, 0);

  //信号量 sem 初始化 第一个1表示进程间同步互斥 第二个1信号量的初始值1
  sem_init(sem, 1, 1);
  pid_t pid = fork();
  if(pid == 0){//子进程
    //p 操作-1
    sem_wait(sem);
    myPrintf("hello");
    //v 操作+1
    sem_post(sem);
  }
  else if(pid > 0){//父进程
    sem_wait(sem);
    myPrintf("world");
    sem_post(sem);
  }
  //回收子进程
  wait(NULL);
  //销毁信号量
  sem_destroy(sem);
  //销毁mmap
  munmap(sem, sizeof(sem_t));
 
  return 0;
}

结果:

编译时需要-pthread


总结:
无名信号量 (sem_t sem) 线程同步互斥。

无名信号量(mmap 匿名映射) 有血缘关系的进程同步互斥

有名信号量 无血缘关系的进程同步互斥

知识点2【有名信号量 没有血缘进程互斥】


1、创建一个有名信号量

 #include <fcntl.h>           /* For O_* constants */
 #include <sys/stat.h>        /* For mode constants */
 #include <semaphore.h>
 //信号量存在
sem_t *sem_open(const char *name, int oflag);
//信号量不存在
sem_t *sem_open(const char *name, int oflag,
                       mode_t mode, unsigned int value);

功能:

        创建一个信号量

参数:

        name:信号量的名字

        oflag:sem_open函数的权限标志

        mode:文件权限(可读、可写、可执行 0777)的设置

        value:信号量的初始值

返回值:

        信号量的地址,失败:SEM_FAILED

2、信号量的关闭:


int sem_close(sem_t *sem);
功能:关闭信号量

参数:信号量的的地址

返回值:成功0 失败-1

3、信号量文件的删除


#include <semaphore.h>
int sem_unlink(const char *name);
功能:删除信号量的文件

参数:信号量的文件名

返回值:成功0 失败-1

4、P操作 sem_wait V操作sem_post 销毁信号量sem_destroy


04_fork01.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>
 
void myPrintf(char *str)
{
    while(*str != 0)
    {
        printf("%c", *str);
        fflush(stdout);
        str++;
        sleep(1);
    }
}
int main()
{
    //互斥需要一个信号量
    sem_t *sem = sem_open("sem", O_RDWR|O_CREAT,0666,1);
    //p操作-1
    sem_wait(sem);
    myPrintf("hello hehe haha");
    //v操作+1
    sem_post(sem);
    
    //关闭信号量
    sem_close(sem);
    return 0;
}


04_fork02.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>
 
void myPrintf(char *str)
{
    while(*str != 0)
    {
        printf("%c", *str);
        fflush(stdout);
        str++;
        sleep(1);
    }
}
int main()
{
    //互斥需要一个信号量
    sem_t *sem = sem_open("sem", O_RDWR|O_CREAT,0666,1);
    //p操作-1
    sem_wait(sem);
    myPrintf("world xixi lala");
    //v操作+1
    sem_post(sem);
    
    //关闭信号量
    sem_close(sem);
    return 0;
}


运行结果:编译的时候记得加-lpthread

知识点3【有名信号量 没有血缘进程同步】


04_fork01.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>
 
void myPrintf(char *str)
{
    while(*str != 0)
    {
        printf("%c", *str);
        fflush(stdout);
        str++;
        sleep(1);
    }
}
int main()
{
    //同步需要2个信号量
    sem_t *sem1 = sem_open("sem1", O_RDWR|O_CREAT,0666,1);
    sem_t *sem2 = sem_open("sem2", O_RDWR|O_CREAT,0666,0);
    
    //p操作-1
    sem_wait(sem1);
    myPrintf("hello hehe haha");
    //v操作+1
    sem_post(sem2);
    
    //关闭信号量
    sem_close(sem1);
    sem_close(sem2);
    return 0;
}


04_fork02.c

#include<stdio.h>
#include<string.h>
#include<stdlib.h>
#include<semaphore.h>
#include<unistd.h>
#include<fcntl.h>
 
void myPrintf(char *str)
{
    while(*str != 0)
    {
        printf("%c", *str);
        fflush(stdout);
        str++;
        sleep(1);
    }
}
int main()
{
    //同步需要2个信号量
    sem_t *sem1 = sem_open("sem1", O_RDWR|O_CREAT,0666,1);
    sem_t *sem2 = sem_open("sem2", O_RDWR|O_CREAT,0666,0);
    //p操作-1
    sem_wait(sem2);
    myPrintf("world xixi lala");
    //v操作+1
    sem_post(sem1);
    
    //关闭信号量
    sem_close(sem1);
    sem_close(sem2);
    
    return 0;
}

线程间

进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

我们先看看PV同步和互斥等基本概念。。。

什么是临界资源?


进程在运行过程中,一般会与其他的进程共享资源,而有些资源具有排他性。一次只能为一个进程所使用,通常把这种一次仅允许一个进程使用的资源称为临界资源(如:打印机、绘图机、一些进程间共享的变量、缓存区)。进程访问临界资源的那段代码称为临界区,也叫临界段。

访问临界资源应遵循如下原则:

  • 1、空闲让进(或有空即进):当进程处于临界区时,可以允许一个请求进出临界区的进程立即进出自己的临界区。
  • 2、忙则等待(或无空则等):当已有进程进入临界区时,其他试图进入临界区的进程必须等待。
  • 3、有限等待:对要求访问临界资源的进程,应保证能在有限的时间内进入自己的临界区。
  • 4、让权等待:当进程不能进入自己的临界区时,应释放处理机。

什么是同步互斥?


1、同步

 同步是合作进程间的直接制约问题。

 进程间的同步是指进程间完成一项任务时直接发生相互作用的关系。



2、互斥

  互斥是申请临界资源进程间的间接制约问题。

  进程互斥是指系统中各进程互斥使用临界资源。

什么是PV操作?


!!!信号量里面的pv我们也可以理解为P是申请资源,而V是释放资源。


    1、P操作定义:
      S:=S-1
     若S>=0,则执行P操作的进程继续执行;
     若S<0,则置该进程为阻塞状态(因为无可以用资源),并将其插入阻塞队列。
    2、操作过程:

  Procedure P(Var S:Semaphore){
   S--;
   if(S<0){
        阻塞该进程;
         将该进程插入信号量S的等待队列;
    }
 }

** Semphore表示所定义的变量是信号量。


     1、 V操作定义:
       S:=S+1
       若S>0,则执行V操作的进程继续执行;
       若S<=0,则从阻塞状态唤醒一个进程,并将其插入就绪队列,然后执行V操作的进程继续执行。
    2、操作过程:
      Procedure V(Var S:Semaphore){
    S++;
    if(S <= 0){
       从信号量的等待队列中取出队首进程;
       将其插入就绪队列;
     }
  }

比较经典的问题比生产者消费者问题、哲学家进餐问题等都是通过缓冲区的互斥访问,同时还需要注意死锁的问题。。。

信号量的特性


信号量(semaphore)与已经介绍过的 IPC 结构不同,它是一个计数器。信号量用于实现进程间的互斥与同步,而不是用于存储进程间通信数据。

信号量用于进程间同步,若要在进程间传递数据需要结合共享内存。

信号量基于操作系统的 PV 操作,程序对信号量的操作都是原子操作。

每次对信号量的 PV 操作不仅限于对信号量值加 1 或减 1,而且可以加减任意正整数。
一个信号量 S 是个整型变量,它除了初始化外只能通过两个标准原子操作:wait () 和 signal() 来访问:

操作 wait() 最初称为 P(荷兰语proberen,测试)
操作 signal() 最初称为 V(荷兰语verhogen,增加)
信号量原型
最简单的信号量是只能取 0 和 1 的变量,这也是信号量最常见的一种形式,叫做二值信号量(Binary Semaphore)。而可以取多个正整数的信号量被称为通用信号量。

Linux 下的信号量函数都是在通用的信号量数组上进行操作,而不是在一个单一的二值信号量上进行操作。

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

 // 创建或获取一个信号量组:若成功返回信号量集ID,失败返回-1
 int semget(key_t key, int num_sems, int sem_flags);
 // 对信号量组进行操作,改变信号量的值:成功返回0,失败返回-1
 int semop(int semid, struct sembuf semoparray[], size_t numops);  
 // 控制信号量的相关信息
 int semctl(int semid, int sem_num, int cmd, ...);


当semget创建新的信号量集合时,必须指定集合中信号量的个数(即num_sems),通常为1; 如果是引用一个现有的集合,则将num_sems指定为 0 。

在semop函数中,sembuf结构的定义如下:

 struct sembuf sops[2];
 int semid;
 
/* Code to set semid omitted */

  sops[0].sem_num = 0;        /* Operate on semaphore 0 */
  sops[0].sem_op = 0;         /* Wait for value to equal 0 */
  sops[0].sem_flg = 0;

  sops[1].sem_num = 0;        /* Operate on semaphore 0 */
  sops[1].sem_op = 1;         /* Increment value by one */
  sops[1].sem_flg = 0;

if (semop(semid, sops, 2) == -1) {
       perror("semop");
       exit(EXIT_FAILURE);
 }


其中 sem_op 是一次操作中的信号量的改变量:

若sem_op > 0,表示进程释放相应的资源数,将 sem_op 的值加到信号量的值上。如果有进程正在休眠等待此信号量,则换行它们。

若sem_op < 0,请求 sem_op 的绝对值的资源。

如果相应的资源数可以满足请求,则将该信号量的值减去sem_op的绝对值,函数成功返回。
当相应的资源数不能满足请求时,这个操作与sem_flg有关。
sem_flg 指定IPC_NOWAIT,则semop函数出错返回EAGAIN。
sem_flg 没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
当相应的资源数可以满足请求,此信号量的semncnt值减1,该信号量的值减去sem_op的绝对值。成功返回;
此信号量被删除,函数smeop出错返回EIDRM;
进程捕捉到信号,并从信号处理函数返回,此情况下将此信号量的semncnt值减1,函数semop出错返回EINTR
若sem_op == 0,进程阻塞直到信号量的相应值为0。
当信号量已经为0,函数立即返回。
如果信号量的值不为0,则依据sem_flg决定函数动作:
sem_flg指定IPC_NOWAIT,则出错返回EAGAIN。
sem_flg没有指定IPC_NOWAIT,则将该信号量的semncnt值加1,然后进程挂起直到下述情况发生:
信号量值为0,将信号量的semzcnt的值减1,函数semop成功返回;
此信号量被删除,函数smeop出错返回EIDRM;
进程捕捉到信号,并从信号处理函数返回,在此情况将此信号量的semncnt值减1,函数semop出错返回EINTR
在semctl函数中的命令有多种,这里就说两个常用的:

SETVAL:用于初始化信号量为一个已知的值。所需要的值作为联合semun的val成员来传递。在信号量第一次使用之前需要设置信号量。

IPC_RMID:删除一个信号量集合。如果不删除信号量,它将继续在系统中存在,即使程序已经退出,它可能在你下次运行此程序时引发问题,而且信号量是一种有限的资源。

实例

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

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 semop(int semid, struct sembuf *sops, unsigned nsops);
void getKey(int id){
    
    struct sembuf sops;
        sops.sem_num = 0;        /* Operate on semaphore 0 */
        sops.sem_op = -1;         /* Wait for value to equal 0 */
        sops.sem_flg = SEM_UNDO;
    semop(id,&sops,1);    
    printf("get key suceed\n");
        if(semop(id, &sops,1) == -1) {
               perror("semop");
               exit(-1);
        }
    
}


void bacKey(int id){
    
    struct sembuf sops;
        sops.sem_num = 0;        /* Operate on semaphore 0 */
        sops.sem_op = 1;         /* Wait for value to equal 0 */
        sops.sem_flg = SEM_UNDO;
    semop(id,&sops,1);    
    printf("back key suceed\n");    
        if(semop(id, &sops, 1) == -1) {
               perror("semop");
               exit(-1);
        }

}

int main(){
    
    key_t key;
    key = ftok(".",2);
    int semid;
//    int semget(key_t key, int nsems, int semflg);
    semid = semget(key,1,IPC_CREAT|0666);//Create semaphore
    //这里信号量集我们就设置了1个
//    int semctl(int semid, int semnum, int cmd, ...);
    union semun initset;
    initset.val = 0; 
    semctl(semid,0,SETVAL,initset);//Initialize the semaphore
    
    int pid = fork();
    while(1){
        if(pid > 0){
            //get key
            getKey(semid);
            printf("this is father\n");
            //back key
            bacKey(semid);
        }else if(pid == 0){
            printf("this is child\n");
            bacKey(semid);
        }else{
            printf("fork error\n");
        }
        sleep(3);
        printf("\n");
    }
    return 0;
}


int semctl(int semid, int semnum, int cmd, …);手册里面说了里面可以包含3个或者4个参数,第四个参数是一个联合体,里面有初始值等配置

如图说明:


实例运行结果:

 
在实例的运行结果中,我们可以看到,先执行child再执行father,因为一开始没有锁,也就是你P操作根本拿不到锁,导致阻塞,进而先去执行child,child执行完之后放锁,父进程才可以执行。

相比较之前没有PV操作的结果,我们可以控制进程的先后顺序,而不是父进程和子进程随意执行。


内核间

概念

Linux内核的信号量在概念和原理上和用户态的System V的IPC机制信号量是相同的,不过他绝不可能在内核之外使用,因此他和System V的IPC机制信号量毫不相干。

如果有一个任务想要获得已经被占用的信号量时,信号量会将其放入一个等待队列(它不是站在外面痴痴地等待而是将自己的名字写在任务队列中)然后让其睡眠。

当持有信号量的进程将信号释放后,处于等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并让其获得信号量。这一点与自旋锁不同,处理器可以去执行其它代码。

应用场景

由于争用信号量的进程在等待锁重新变为可用时会睡眠,所以信号量适用于锁会被长时间持有的情况;相反,锁被短时间持有时,使用信号量就不太适宜了,因为睡眠、维护等待队列以及唤醒所花费的开销可能比锁占用的全部时间表还要长。

举2个生活中的例子:

  1. 我们坐火车从南京到新疆需要2天的时间,这个'任务'特别的耗时,只能坐在车上等着车到站,但是我们没有必要一直睁着眼睛等,理想的情况就是我们上车就直接睡觉,醒来就到站(看过《异形》的读者会深有体会),这样从人(用户)的角度来说,体验是最好的,对比于进程,程序在等待一个耗时事件的时候,没有必须要一直占用CPU,可以暂停当前任务使其进入休眠状态,当等待的事件发生之后再由其他任务唤醒,类似于这种场景采用信号量比较合适。

  2. 我们有时候会等待电梯、洗手间,这种场景需要等待的时间并不是很多,如果我们还要找个地方睡一觉,然后等电梯到了或者洗手间可以用了再醒来,那很显然这也没有必要,我们只需要排好队,刷一刷抖音就可以了,对比于计算机程序,比如驱动在进入中断例程,在等待某个寄存器被置位,这种场景需要等待的时间往往很短暂,系统开销甚至远小于进入休眠的开销,所以这种场景采用自旋锁比较合适。

关于信号量和自旋锁,以及死锁问题,我们后面会再详细讨论。

使用方法

一个任务要想访问共享资源,首先必须得到信号量,获取信号量的操作将把信号量的值减1,若当前信号量的值为负数,表明无法获得信号量,该任务必须挂起在 该信号量的等待队列等待该信号量可用;若当前信号量的值为非负数,表示能获得信号量,因而能即时访问被该信号量保护的共享资源。

当任务访问完被信号量保护的共享资源后,必须释放信号量,释放信号量通过把信号量的值加1实现,如果信号量的值为非正数,表明有任务等待当前信号量,因此他也唤醒所有等待该信号量的任务。

内核信号量的构成

内核信号量类似于自旋锁,因为当锁关闭着时,它不允许内核控制路径继续进行。然而,当内核控制路径试图获取内核信号量锁保护的忙资源时,相应的进程就被挂起。只有在资源被释放时,进程才再次变为可运行。

只有可以睡眠的函数才能获取内核信号量;中断处理程序和可延迟函数都不能使用内核信号量。

内核信号量是struct semaphore类型的对象,在内核源码中位于include\linux\semaphore.h文件

struct semaphore{
    raw_spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
}
成员描述
lock在2.6.33之后的版本,内核加入了raw_spin_lock系列,使用方法和spin_lock系列一模一样,只是参数spinlock_t变为了raw_spinlock_t
count相当于信号量的值,大于0,资源空闲;等于0,资源忙,但没有进程等待这个保护的资源;小于0,资源不可用,并至少有一个进程等待资源
wait_list内核链表,当前获得信号量的任务会与该成员一起注册到等待的链表中

信号量的API

初始化

DECLARE_MUTEX(name)

该宏声明一个信号量name并初始化他的值为1,即声明一个互斥锁。

DECLARE_MUTEX_LOCKED(name)

该宏声明一个互斥锁name,但把他的初始值设置为0,即锁在创建时就处在已锁状态。因此对于这种锁,一般是先释放后获得。

void sema_init (struct semaphore *sem, int val);

该函用于数初始化设置信号量的初值,他设置信号量sem的值为val。

注意:

val设置为1说明只有一个持有者,这种信号量叫二值信号量或者叫互斥信号量。

我们还允许信号量可以有多个持有者,这种信号量叫计数信号量,在初始化时要说明最多允许有多少个持有者也可以把信号量中的val初始化为任意的正数值n,在这种情况下,最多有n个进程可以并发地访问这个资源。

void init_MUTEX (struct semaphore *sem);

该函数用于初始化一个互斥锁,即他把信号量sem的值设置为1。

void init_MUTEX_LOCKED (struct semaphore *sem);

该函数也用于初始化一个互斥锁,但他把信号量sem的值设置为0,即一开始就处在已锁状态。

PV操作

获取信号量(P)

void down(struct semaphore * sem);

该函数用于获得信号量sem,他会导致调用该函数的进程睡眠,因此不能在中断上下文(包括IRQ上下文和softirq上下文)使用该函数。该函数将把sem的值减1,如果信号量sem的值非负,就直接返回,否则调用者将被挂起,直到别的任务释放该信号量才能继续运行。

int down_interruptible(struct semaphore * sem);

该函数功能和down类似,不同之处为,down不会被信号(signal)打断,但down_interruptible能被信号打断,因此该函数有返回值来区分是正常返回还是被信号中断,如果返回0,表示获得信号量正常返回,如果被信号打断,返回-EINTR。

int down_trylock(struct semaphore * sem);

该函数试着获得信号量sem,如果能够即时获得,他就获得该信号量并返回0,否则,表示不能获得信号量sem,返回值为非0值。因此,他不会导致调用者睡眠,能在中断上下文使用。

int down_killable(struct semaphore *sem);
int down_timeout(struct semaphore *sem, long jiffies);
int down_timeout_interruptible(struct semaphore *sem, long jiffies);

释放内核信号量(V)

void up(struct semaphore * sem);

该函数释放信号量sem,即把sem的值加1,如果sem的值为非正数,表明有任务等待该信号量,因此唤醒这些等待者。

补充

int down_interruptible(struct semaphore *sem)

这个函数的功能就是获得信号量,如果得不到信号量就睡眠,此时没有信号打断,那么进入睡眠。但是在睡眠过程中可能被信号打断,打断之后返回-EINTR,主要用来进程间的互斥同步。

下面是该函数的注释:

/**
* down_interruptible - acquire the semaphore unless interrupted
* @sem: the semaphore to be acquired
*
* Attempts to acquire the semaphore. If no more tasks are allowed to
* acquire the semaphore, calling this function will put the task to sleep.
* If the sleep is interrupted by a signal, this function will return -EINTR.
* If the semaphore is successfully acquired, this function returns 0.
*/

一个进程在调用down_interruptible()之后,如果sem<0,那么就进入到可中断的睡眠状态并调度其它进程运行, 但是一旦该进程收到信号,那么就会从down_interruptible函数中返回。并标记错误号为:-EINTR。

一个形象的比喻:传入的信号量为1好比天亮,如果当前信号量为0,进程睡眠,直到(信号量为1)天亮才醒,但是可能中途有个闹铃(信号)把你闹醒。

又如:小强下午放学回家,回家了就要开始吃饭嘛,这时就会有两种情况:
情况一:饭做好了,可以开始吃;
情况二:当他到厨房去的时候发现妈妈还在做,
妈妈就对他说:“你先去睡会,待会做好了叫你。”
小强就答应去睡会,不过又说了一句:“睡的这段时间要是小红来找我玩,你可以叫醒我。”
小强就是down_interruptible,想吃饭就是获取信号量,睡觉对应这里的休眠,而小红来找我玩就是中断休眠。

使用可被中断的信号量版本的意思是,万一出现了semaphore的死锁,还有机会用ctrl+c发出软中断,让等待这个内核驱动返回的用户态进程退出。而不是把整个系统都锁住了。在休眠时,能被中断信号终止,这个进程是可以接受中断信号的!

比如你在命令行中输入# sleep 10000,按下ctrl + c,就给上面的进程发送了进程终止信号。信号发送给用户空间,然后通过系统调用,会把这个信号传给递给驱动。信号只能发送给用户空间,无权直接发送给内核的,那1G的内核空间,我们是无法直接去操作的。

内核信号量的使用例程

场景1

在驱动程序中,当多个线程同时访问相同的资源时(驱动中的全局变量时一种典型的共享资源),可能会引发“竞态“,因此我们必须对共享资源进行并发控制。Linux内核中

解决并发控制的最常用方法是自旋锁与信号量(绝大多数时候作为互斥锁使用)。

在这里插入图片描述

场景2

有时候我们希望设备只能被一个进程打开,当设备被占用的时候,其他设备必须进入休眠。

信号处理示意图

在这里插入图片描述

如上图:

  1. 进程A首先通过open()打开设备文件,调用到内核的hello_open(),并调用down_interruptible(),因为此时信号量没有被占用,所以进程A可以获得信号量;

  2. 进程A获得信号量之后继续处理原有任务,此时进程B也要通过open()打开设备文件,同样调用内核函数hello_open(),但此时信号量获取不到,于是进程B被阻塞;

  3. 进程A任务执行完毕,关闭设备文件,并通过up()释放信号量,于是进程B被唤醒,并得以继续执行剩下的任务,

  4. 进程B执行完任务,释放设备文件,通过up()释放信号量

代码如下:

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/semaphore.h>

static int major = 250;
static int minor = 0;
static dev_t devno;
static struct cdev cdev;


static struct class *cls;
static struct device *test_device;

static struct semaphore sem;
static int hello_open (struct inode *inode, struct file *filep)
{
    
    if(down_interruptible(&sem))//p
    {
        return -ERESTARTSYS;
    }
      return 0;
}
static int hello_release (struct inode *inode, struct file *filep)
{
    up(&sem);//v
    return 0;
}
static struct file_operations hello_ops =
{
    .open = hello_open,
    .release = hello_release,
};
static int hello_init(void)
{
    int result;
    int error;    
    printk("hello_init \n");
    result = register_chrdev( major, "hello", &hello_ops);
    if(result < 0)
    {
        printk("register_chrdev fail \n");
        return result;
    }
    devno = MKDEV(major,minor);
    cls = class_create(THIS_MODULE,"helloclass");
    if(IS_ERR(cls))
    {
        unregister_chrdev(major,"hello");
        return result;
    }
    test_device = device_create(cls,NULL,devno,NULL,"test");
    if(IS_ERR(test_device ))
    {
        class_destroy(cls);
        unregister_chrdev(major,"hello");
        return result;
    }
    sem_init(&sem,1);
    return 0;
}
static void hello_exit(void)
{
    printk("hello_exit \n");
    device_destroy(cls,devno);    
    class_destroy(cls);
    unregister_chrdev(major,"hello");
    return;
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("daniel.peng");

测试程序 test.c

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
    int fd;
    
    printf("before open\n ");    
    fd = open("/dev/test",O_RDWR);  //原子变量  0
    if(fd<0)
    {
        perror("open fail \n");
        return;
    }
    printf("open ok ,sleep......\n ");    
    sleep(20);
    printf("wake up from sleep!\n ");        
    close(fd);   //加为1
}

编译步骤

1 make 生成 hello.ko

2 gcc test.c -o a

3 gcc test.c -o b

测试步骤

  1. 安装驱动
insmod hello.ko
  1. 先运行进程A,在运行进程B

可见进程A成功打开设备,在进程A sleep期间会一直占有该字符设备,进程B由于无法获得信号量,进入休闲,结合代码可知,进程B被阻塞在函数open()中。

  1. 进程A 结束了sleep,并释放字符设备以及信号量,进程B被唤醒获得信号量,并成功打开了字符设备。

  1. 进程B执行完sleep函数后退出,并释放字符设备和信号量。

读-写信号量

跟自旋锁一样,信号量也有区分读-写信号量之分。

如果一个读写信号量当前没有被写者拥有并且也没有写者等待读者释放信号量,那么任何读者都可以成功获得该读写信号量;否则,读者必须被挂起直到写者释放该信号量。如果一个读写信号量当前没有被读者或写者拥有并且也没有写者等待该信号量,那么一个写者可以成功获得该读写信号量,否则写者将被挂起,直到没有任何访问者。因此,写者是排他性的,独占性的。

读写信号量有两种实现,一种是通用的,不依赖于硬件架构,因此,增加新的架构不需要重新实现它,但缺点是性能低,获得和释放读写信号量的开销大;另一种是架构相关的,因此性能高,获取和释放读写信号量的开销小,但增加新的架构需要重新实现。在内核配置时,可以通过选项去控制使用哪一种实现。

读写信号量的相关API:

DECLARE_RWSEM(name)

该宏声明一个读写信号量name并对其进行初始化。

void init_rwsem(struct rw_semaphore *sem);

该函数对读写信号量sem进行初始化。

void down_read(struct rw_semaphore *sem);

读者调用该函数来得到读写信号量sem。该函数会导致调用者睡眠,因此只能在进程上下文使用。

int down_read_trylock(struct rw_semaphore *sem);

该函数类似于down_read,只是它不会导致调用者睡眠。它尽力得到读写信号量sem,如果能够立即得到,它就得到该读写信号量,并且返回1,否则表示不能立刻得到该信号量,返回0。因此,它也可以在中断上下文使用。

void down_write(struct rw_semaphore *sem);

写者使用该函数来得到读写信号量sem,它也会导致调用者睡眠,因此只能在进程上下文使用。

int down_write_trylock(struct rw_semaphore *sem);

该函数类似于down_write,只是它不会导致调用者睡眠。该函数尽力得到读写信号量,如果能够立刻获得,就获得该读写信号量并且返回1,否则表示无法立刻获得,返回0。它可以在中断上下文使用。

void up_read(struct rw_semaphore *sem);

读者使用该函数释放读写信号量sem。它与down_read或down_read_trylock配对使用。

如果down_read_trylock返回0,不需要调用up_read来释放读写信号量,因为根本就没有获得信号量。

void up_write(struct rw_semaphore *sem);

写者调用该函数释放信号量sem。它与down_write或down_write_trylock配对使用。如果down_write_trylock返回0,不需要调用up_write,因为返回0表示没有获得该读写信号量。

void downgrade_write(struct rw_semaphore *sem);

该函数用于把写者降级为读者,这有时是必要的。因为写者是排他性的,因此在写者保持读写信号量期间,任何读者或写者都将无法访问该读写信号量保护的共享资源,对于那些当前条件下不需要写访问的写者,降级为读者将,使得等待访问的读者能够立刻访问,从而增加了并发性,提高了效率。

读写信号量适于在读多写少的情况下使用,在linux内核中对进程的内存映像描述结构的访问就使用了读写信号量进行保护。

 

  • 0
    点赞
  • 10
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值