我们已经准备好了,你呢?

2026我们与您携手共赢,为您的企业形象保驾护航!

概念

Linux内核的信号量在概念和原理上与用户态V的IPC机制信号量相同,但是它永远不能在内核之外使用,所以与V的IPC机制信号量没有任何关系。

如果一个任务想要获取一个已经被占用的信号量,信号量就会把它放到一个等待队列中(它不会傻傻的站在外面等待,而是把自己的名字写到任务队列里),然后让它休眠。

当持有信号量的进程释放信号时,等待队列中的一个任务将被唤醒(因为队列中可能不止一个任务),并允许获取信号量。这和自旋锁不同,处理器可以执行其他代码。

应用场景

由于竞争信号量的进程在等待锁再次可用时会进行睡眠,所以信号量适用于长时间持有锁的情况;相反,当持有锁的时间很短时,就不宜使用信号量,因为睡眠、维护等待队列以及唤醒的开销可能比锁占用的总时间还要长。

以下是现实生活中的两个例子:

从南京坐火车到新疆要两天时间,这个“任务”特别耗时,我们只能坐在车厢里等火车到站,但又不需要睁着眼睛等,最理想的情况是上车后直接睡觉,到站醒来(看过《异形》的读者对这一点会深有体会)。从人(用户)的角度看,这是最好的体验。相比进程,程序在等待耗时事件时,不必一直占用 CPU,可以暂停当前任务进入睡眠状态,等等待的事件发生后再由其他任务唤醒。像这样的场景,使用信号量再合适不过了。

有时我们会等电梯或者上厕所,这种情况下等待的时间并不是很长,如果要找个地方睡一觉,然后等电梯到了或者厕所空出来了再醒来,那这显然是没有必要的,排队看个抖音就行了。相比于计算机程序,比如驱动程序进入中断例程,等待某个寄存器被设置,这种场景下的等待时间往往很短,系统开销甚至比进入休眠的开销小很多,所以这种场景下使用自旋锁更合适。

我们稍后会详细讨论信号量、自旋锁和死锁问题。

指示

任务要想访问共享资源,必须先获取信号量。获取信号量的操作会将信号量的值减1。如果信号量的当前值为负数,则表示无法获取信号量,必须将任务挂起在信号量的等待队列中,等待信号量变为可用;如果信号量的当前值为非负数,则表示可以获取信号量,从而可以立即访问该信号量保护的共享资源。

当一个任务访问完该信号量保护的共享资源后,必须释放该信号量。信号量的释放是通过对信号量的值加 1 来实现的。如果信号量的值为非正数,则表示有任务在等待当前信号量,因此也会唤醒所有等待该信号量的任务。

内核信号量的组成

内核信号量与自旋锁类似,在锁关闭时,它不允许内核控制路径继续运行。但是,当内核控制路径尝试获取受内核信号量锁保护的繁忙资源时,相应的进程将被暂停。只有当资源被释放时,进程才会再次变为可运行状态。

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

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

struct semaphore{
    raw_spinlock_t        lock;
    unsigned int        count;
    struct list_head    wait_list;
}

会员描述

在2.6.33之后的版本,内核增加了,使用方式和完全相同,只是参数改为

数数

相当于信号量的值,大于0,资源空闲;等于0,资源繁忙,但是没有进程在等待受保护的资源;小于0,资源不可用,并且至少有一个进程在等待该资源

内核链表,当前获取信号量的任务会和此成员一起注册到等待链表中

信号量 API

初始化

DECLARE_MUTEX(name)

该宏声明一个信号量名称,并将其值初始化为1,即声明一个互斥锁。

DECLARE_MUTEX_LOCKED(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上下文和中断上下文)中不能使用此函数。该函数会将sem的值减1,如果信号量sem的值非负则直接返回,否则调用者会被挂起,直到有其他任务释放信号量才能继续运行。

int down_interruptible(struct semaphore * sem);

该函数和down类似,只不过down不会被()打断,但是可以被信号打断。所以该函数有一个返回值来区分是正常返回还是被信号打断,如果返回0,表示获取了信号量,正常返回,如果被信号打断,则返回-EINTR。

int down_trylock(struct semaphore * sem);

此函数尝试获取信号量sem,如果可以立即获取则获取信号量并返回0,否则表示无法获取信号量sem,返回值非零,因此不会导致调用者睡眠,可以在中断上下文中使用。

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.
*/

在进程调用()之后,如果 sem

打个形象的比喻:到来的信号量1就好比是黎明,如果当前信号量为0,进程就睡到天亮(信号量为1),但是中途一个闹钟(信号)可能就把你吵醒了。

举个例子:小强下午放学回家,到家就开始吃饭,这时候就有两种情况:情况一:饭做好了,可以开始吃饭了;情况二:去厨房的时候发现妈妈还在做饭,妈妈就对他说:“先睡吧,做好了再叫你。”小强同意去睡个午觉,但是接着又说:“如果这段时间小红来找我玩,你就可以叫醒我了。”小强的意思是,如果他想吃饭,他就拿到信号量,睡觉对应的是这里的休眠,而小红来找我玩,则是打断了休眠。

使用可中断信号量版本的意义在于,如果出现死锁,还有机会用ctrl+c发出软中断,让等待内核驱动返回的用户态进程退出,而不是锁住整个系统。在睡眠模式时,可以用中断信号终止,而这个进程是可以接受中断信号的!

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

内核信号量使用例程

场景 1

在驱动程序中,当多个线程同时访问同一个资源(驱动程序中的全局变量就是典型的共享资源)时,可能会出现“竞争状态”,所以我们必须对共享资源进行并发控制。

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

linux宏内核微内核_linux 内核版本号宏_linux内核是宏内核

在此处插入图像描述

场景 2

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

信号处理图

linux宏内核微内核_linux 内核版本号宏_linux内核是宏内核

在此处插入图像描述

如上图:

进程A首先通过open()打开设备文件,调用内核的(),调用(),由于此时信号量没有被占用,因此进程A可以获得该信号量;

进程A获取到信号量之后,继续处理原任务,此时进程B也需要通过open()打开设备文件,并调用内核函数(),但此时无法获取信号量,因此进程B被阻塞;

进程A完成任务后,通过up()关闭设备文件,释放信号量。然后进程B被唤醒,可以继续执行剩余任务。

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

代码如下:

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

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 
#include 
#include 
#include 
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 测试.c -oa

3 gcc 测试.c -ob

测试步骤

安装驱动程序

insmod hello.ko

先运行进程A,再运行进程B

linux内核是宏内核_linux 内核版本号宏_linux宏内核微内核

可以看出,进程A成功打开设备,并且在A进程睡眠期间会占用字符设备。进程B无法获取信号量,进入空闲状态。结合代码可以看出,进程B被阻塞在函数open()中。

进程A结束睡眠,释放字符设备和信号量。进程B被唤醒,获取信号量,并成功打开字符设备。

linux宏内核微内核_linux内核是宏内核_linux 内核版本号宏

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

linux 内核版本号宏_linux宏内核微内核_linux内核是宏内核

读写信号量

与自旋锁一样,信号量也区分读写信号量。

如果读写信号量当前不属于任何写入者,并且没有写入者在等待读取者释放信号量,则任何读取者都可以成功获取读写信号量;否则,读取者必须被挂起,直到写入者释放信号量。如果读写信号量当前不属于任何读取者或写入者,并且没有写入者在等待信号量,则写入者可以成功获取读写信号量,否则,写入者将被挂起,直到没有访问者。因此,写入者是独占和垄断的。

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

读写信号量的API:

DECLARE_RWSEM(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);

该函数与 sem() 类似,但不会导致调用者进入睡眠状态。它会尝试获取读写信号量 sem。如果可以立即获取,则获取读写信号量并返回 1。否则,无法立即获取信号量并返回 0。因此,它也可以在中断上下文中使用。

void down_write(struct rw_semaphore *sem);

写入者使用该函数获取读写信号量 sem。它还会导致调用者进入睡眠状态,因此只能在进程上下文中使用。

int down_write_trylock(struct rw_semaphore *sem);

该函数与类似,只不过不会导致调用者睡眠。该函数尝试获取读写信号量,如果可以立即获取则获取读写信号量并返回1,否则无法立即获取则返回0。可以在中断上下文中使用。

void up_read(struct rw_semaphore *sem);

读者使用该函数释放读写信号量sem。它与or配合使用。

如果返回 0,则不需要调用来释放读写信号量,因为尚未获取信号量。

void up_write(struct rw_semaphore *sem);

写入者调用此函数释放信号量sem,与or配对使用,如果返回0则不需要调用,因为返回0代表没有获取到读写信号量。

void downgrade_write(struct rw_semaphore *sem);

此函数用于将写入者降级为读取者,这有时是必要的。由于写入者是独占的,因此在写入者持有读写信号量时,任何读取者或写入者都无法访问受读写信号量保护的共享资源。对于那些在当前条件下不需要写入访问权限的写入者,降级为读取者将允许等待访问的读取者立即访问,从而增加并发性并提高效率。

读写信号量适合用于读操作多于写的场合,在Linux内核中,读写信号量用于保护对进程的内存映像描述结构的访问。

-结束-

二维码
扫一扫在手机端查看

本文链接:https://by928.com/2447.html     转载请注明出处和本文链接!请遵守 《网站协议》
我们凭借多年的网站建设经验,坚持以“帮助中小企业实现网络营销化”为宗旨,累计为4000多家客户提供品质建站服务,得到了客户的一致好评。如果您有网站建设、网站改版、域名注册、主机空间、手机网站建设、网站备案等方面的需求,请立即点击咨询我们或拨打咨询热线: 13761152229,我们会详细为你一一解答你心中的疑难。

项目经理在线

我们已经准备好了,你呢?

2020我们与您携手共赢,为您的企业形象保驾护航!

在线客服
联系方式

热线电话

13761152229

上班时间

周一到周五

公司电话

二维码
微信
线