程序项目代做,有需求私信(vue、React、Java、爬虫、电路板设计、嵌入式linux等)

linux同步机制-信号量

一、信号量(semaphore)

1.1 什么是信号量

信号量本质上是一个计数器,它用来记录对某个资源的存取状态。一般来说,为了获取共享资源,进程需要执行下列操作:

  • 测试控制该资源的信号量;
  • 如果信号量的值为正,则允许操作该资源,并且信号量值减1;
  • 如果信号量为0,则资源目前不可用,进程进入休眠状态,直至信号量值大于0,进程被唤醒,转入上一步;
  • 当进程不在使用一个信号量控制的资源时,信号量的值加1;

1.2 信号量的工作原理

针对信号量只能进行两种操作,即PV操作,PV操作由P操作原语和V操作原子组成(原子是不可中断的过程),对信号量进行操作,具体定义如下:

  • P(s):如果s的值大于零,就给它减1;如果它的值为零,则将该进程设置为等待状态,排入等待队列;
  • V(s):如果有其它进程因等待s而被挂起,就让它恢复运行,如果没有进程因等待s而挂起,就给它加1;

1.3 信号量的使用

定义信号量:

struct semaphore sema;

DEFINE_SEAMPHORE宏用于定义一个信号量,并设置信号量的值为1:

DEFINE_SEAMPHORE(name)

sema_init 函数用于初始化信号量,并设置信号量sem的值为val:

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

down函数用于获得信号量sem,它会导致进程睡眠,不能在中断上下文中使用,不然会导致中断处理程序休眠(down函数目前已不建议使用):

int down(struct semaphore * sem);

down_interruptible函数功能与down类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠后,进程状态被设置为TASK_INTERRUPTIBLE,该类型的睡眠是可以被信号打断的。

如果返回0,表示获得信号量;如果被信号打断,返回EINTR。

int down_interruptible(struct semaphore * sem);

down_trylock函数尝试获得信号量sem,如果能够立刻获得,它就获得该信号量并返回0,否则,返回正数。它不会导致调用者睡眠,可以在中断上下文使用:

int down_trylock(struct semaphore * sem);

up函数释放信号量sem,唤醒等待者。

void up(struct semaphore * sem);

二、信号量的实现源码

2.1 struct semaphore结构体

维护信号量状态的是Linux内核操作系统而不是用户进程。我们可以从头文件include/linux/semaphore.h:中看到内核用来维护信号量状态的各个结构的定义。信号量结构体定义:

/* Please don't access any members of this structure directly */
struct semaphore {
        raw_spinlock_t          lock;
        unsigned int            count;
        struct list_head        wait_list;
};

lock是一个自旋锁结构,其实现原理在后面的章节介绍。count用来保存可用资源的数量。

list_head是一个双向链表,使用该等待列表保存因获取不到信号量而进行睡眠的进程:

struct list_head{
    struct list_head  *next,*prev;
}

再来看一下DEFINE_SEMAPHORE宏定义初始化信号量成员lock、count、wait_list:

#define __SEMAPHORE_INITIALIZER(name, n)                                \
{                                                                       \
        .lock           = __RAW_SPIN_LOCK_UNLOCKED((name).lock),        \
        .count          = n,                                            \
        .wait_list      = LIST_HEAD_INIT((name).wait_list),             \
}

#define DEFINE_SEMAPHORE(name)  \
        struct semaphore name = __SEMAPHORE_INITIALIZER(name, 1)

static inline void sema_init(struct semaphore *sem, int val)
{
        static struct lock_class_key __key;
        *sem = (struct semaphore) __SEMAPHORE_INITIALIZER(*sem, val);
        lockdep_init_map(&sem->lock.dep_map, "semaphore->lock", &__key, 0);
}

2.2 down函数

再来看看一下down函数,定义在kernel/locking/semaphore.c:文件中,该函数是用来获取信号量的,也是一个原子操作:

/**
 * down - acquire the semaphore
 * @sem: the semaphore to be acquired
 *
 * Acquires the semaphore.  If no more tasks are allowed to acquire the
 * semaphore, calling this function will put the task to sleep until the
 * semaphore is released.
 *
 * Use of this function is deprecated, please use down_interruptible() or
 * down_killable() instead.
 */
void down(struct semaphore *sem)
{
        unsigned long flags;

        raw_spin_lock_irqsave(&sem->lock, flags);
        if (likely(sem->count > 0))
                sem->count--;
        else
                __down(sem);
        raw_spin_unlock_irqrestore(&sem->lock, flags);
}

这里实际上内部调用raw_spin_lock_irqsave、raw_spin_unlock_irqrestore这个两个函数来实现进程同步的,其实现原理是自旋锁。自旋锁可以保证在这两个函数中的代码可以独占的执行,具体实现原理这块内容我们会在自旋锁里面单独介绍。

如果获取不到信号量,__down函数调用 __down_common(sem, TASK_UNINTERRUPTIBLE, MAX_SCHEDULE_TIMEOUT);

/*
 * Because this function is inlined, the 'state' parameter will be
 * constant, and thus optimised away by the compiler.  Likewise the
 * 'timeout' parameter for the cases without timeouts.
 */
static inline int __sched __down_common(struct semaphore *sem, long state,
                                                                long timeout)
{
        struct semaphore_waiter waiter;

        list_add_tail(&waiter.list, &sem->wait_list);
        waiter.task = current;
        waiter.up = false;

        for (;;) {
                if (signal_pending_state(state, current))
                        goto interrupted;
                if (unlikely(timeout <= 0))
                        goto timed_out;
                __set_current_state(state);
                raw_spin_unlock_irq(&sem->lock);
                timeout = schedule_timeout(timeout); // CPU调度、无限睡眠,导致进程切换,开销较大
                raw_spin_lock_irq(&sem->lock);
                if (waiter.up)
                        return 0;
        }

 timed_out:
        list_del(&waiter.list);
        return -ETIME;

 interrupted:
        list_del(&waiter.list);
        return -EINTR;
}

在__down_common函数中会做以下事情:

  • 将当前进程加入信号量等待列表wait_list中;
  • 将当前进程设置为睡眠状态,并且进程状态被设置为TASK_UNINTERRUPTIBLE;
  • 执行CPU调度,当其它进程调用up函数释放信号量,才会唤醒第一个被等待的进程;

2.3 up函数

最后我们再来看一下up函数:

/**
 * up - release the semaphore
 * @sem: the semaphore to release
 *
 * Release the semaphore.  Unlike mutexes, up() may be called from any
 * context and even by tasks which have never called down().
 */
void up(struct semaphore *sem)
{
        unsigned long flags;

        raw_spin_lock_irqsave(&sem->lock, flags);
        if (likely(list_empty(&sem->wait_list)))
                sem->count++;
        else
                __up(sem);
        raw_spin_unlock_irqrestore(&sem->lock, flags);
}

这里同样使用了自旋锁,在临界区代码中, 判断是否有等待的进程,如果有的话,会执行__up函数:

static noinline void __sched __up(struct semaphore *sem)
{
        struct semaphore_waiter *waiter = list_first_entry(&sem->wait_list,
                                                struct semaphore_waiter, list);
        list_del(&waiter->list);
        waiter->up = true;
        wake_up_process(waiter->task);
}

这里首先获取第一个信号量等待者,然后将其从信号量等待列表lwait_head中移除,并设置 waiter->up = true,然后唤醒semaphore_waiter ->task指向的进程。

三、信号量示例程序

3.1 驱动程序

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

#define OK   (0)
#define ERROR  (-1)

/* 信号量 */
static DEFINE_SEMAPHORE(sema);

int hello_open(struct inode *p, struct file *f)
{
    if(down_trylock(&sema) > 0){
        printk("device busy,hello_open failed");
        return ERROR;
    }
    printk("hello_open\n");
    return 0;
}

ssize_t hello_write(struct file *f, const char __user *u, size_t s, loff_t *l)
{
    printk("hello_write\n");
    return 0;
}

ssize_t hello_read(struct file *f, char __user *u, size_t s, loff_t *l)
{
    printk("hello_read\n");
    return 0;
}

int hello_close(struct inode *inode, struct file *file)
{
    /* 释放信号量 */
    up(&sema);  
    return 0;
}

struct file_operations hello_fops = {
    .owner   =   THIS_MODULE,
    .open    =   hello_open,
    .read    =   hello_read,
    .write   =   hello_write,
    .release =   hello_close,
};

dev_t devid;                      // 起始设备编号
struct cdev hello_cdev;          // 保存操作结构体的字符设备 
struct class *hello_cls;

int hello_init(void)      // 也可以在这个函数中调用sema_init初始化信号量
{
    
    /* 动态分配字符设备: (major,0) */
    if(OK == alloc_chrdev_region(&devid, 0, 1,"hello")){   // ls /proc/devices看到的名字
        printk("register_chrdev_region ok\n");
    }else {
        printk("register_chrdev_region error\n");
        return ERROR;
    }
    
     cdev_init(&hello_cdev, &hello_fops);
     cdev_add(&hello_cdev, devid, 1);


    /* 创建类,它会在sys目录下创建/sys/class/hello这个类  */
     hello_cls = class_create(THIS_MODULE, "hello");
     if(IS_ERR(hello_cls)){
         printk("can't create class\n");
         return ERROR;
     }
    /* 在/sys/class/hello下创建hellos设备,然后mdev通过这个自动创建/dev/hello这个设备节点 */
     device_create(hello_cls, NULL, devid, NULL, "hello"); 

     return 0;
}

void __exit hello_exit(void)
{
    printk("hello driver exit\n");
    /* 注销类、以及类设备 /sys/class/hello会被移除*/
    device_destroy(hello_cls, devid);
    class_destroy(hello_cls);

    cdev_del(&hello_cdev);
    unregister_chrdev_region(devid, 1);
    return;
}


module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");

3.2 应用程序

#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>

int main(int argc,char **argv)
{
    int fd;
    int val = 1;
    fd = open("/dev/hello",O_RDWR);
    if(fd == -1){
        printf("can't open!\n");
    }else{
        printf("open success,PID=%d\n",getpid());
        sleep(10);  // 10s
    }
    return 0;
}

3.3 测试

如下图所示,3个进程同时访问时,有一个进程访问成功,两外两个进程都访问失败了:

[root@zy:/]# ./main &hello_open
open success,PID=61
[root@zy:/]# ./main &
[root@zy:/]# can't open!
[root@zy:/]# ./main &
device busy,hello_open failed
can't open!
[root@zy:/]# 

 参考文章

[1]Linux下进程间通信方式——信号量(Semaphore)

[2]七、Linux驱动之并发控制

[3]10.按键之互斥、阻塞机制(详解)

posted @ 2022-02-23 19:32  大奥特曼打小怪兽  阅读(490)  评论(0编辑  收藏  举报
如果有任何技术小问题,欢迎大家交流沟通,共同进步