同步与互斥

本文详细介绍了原子操作、原子变量、自旋锁、信号量、互斥锁以及非阻塞IO等并发控制机制,包括它们的定义、API函数和注意事项,特别关注了在中断和多线程环境中的使用场景。
摘要由CSDN通过智能技术生成

原子操作

保证操作的原子性,从汇编层面上不可被打断

 typedef struct {
     int counter;
 } atomic_t;

原子变量操作

ATOMIC_INIT(int i)                         //定义原子变量的时候对其初始化。
int atomic_read(atomic_t *v)                 //读取 v 的值,并且返回。
void atomic_set(atomic_t *v, int i)            //向 v 写入 i 值。
void atomic_add(int i, atomic_t *v)             //给 v 加上 i 值。
void atomic_sub(int i, atomic_t *v)             //从 v 减去 i 值。
void atomic_inc(atomic_t *v)                 //给 v 加 1,也就是自增。
void atomic_dec(atomic_t *v)                 //从 v 减 1,也就是自减
int atomic_dec_return(atomic_t *v)         //从 v 减 1,并且返回 v 的值。
int atomic_inc_return(atomic_t *v)             //给 v 加 1,并且返回 v 的值。
int atomic_sub_and_test(int i, atomic_t *v)     //从 v 减 i,如果结果为 0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v)         //从 v 减 1,如果结果为 0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v)         //给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v)    //给 v 加 i,如果结果为负就返回真,否则返回假

原子位操作

void set_bit(int nr, void *p)             //将 p 地址的第 nr 位置 1。
void clear_bit(int nr,void *p)             //将 p 地址的第 nr 位清零。
void change_bit(int nr, void *p)         //将 p 地址的第 nr 位进行翻转。
int test_bit(int nr, void *p)             //获取 p 地址的第 nr 位的值。
int test_and_set_bit(int nr, void *p)     //将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值。
int test_and_clear_bit(int nr, void *p)     //将 p 地址的第 nr 位清零,并且返回 nr 位原来的值。
int test_and_change_bit(int nr, void *p)     //将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值。

自旋锁

spinlock,如果没有获取到锁,就会一直自旋,忙等待,不会休眠,知道获取到锁,多用于不可休眠的互斥场景,如中断;
使用自旋锁需要注意的是,在获取到锁之后不能休眠,也不能阻塞,否则可能会造成死锁

基本API函数

DEFINE_SPINLOCK(spinlock_t lock) //定义并初始化一个自选变量
int spin_lock_init(spinlock_t *lock) //初始化自旋锁
void spin_lock(spinlock_t *lock) //获取指定的自旋锁,也叫做加锁
void spin_unlock(spinlock_t *lock) //释放指定的自旋锁
int spin_trylock(spinlock_t *lock) //尝试获取指定的自旋锁,如果没有获取到就返回 0
int spin_is_locked(spinlock_t *lock)//检查指定的自旋锁是否被获取,如果没有被获取就返回非 0,否则返回 0

中断中的spinlock

由于中断发生期间不能长时间执行,且防止获取到锁后被中断打断,中断服务函数也获取同一个锁,但获取不到导致死锁,所以有了以下API

void spin_lock_irq(spinlock_t *lock) //禁止本地中断,并获取自旋锁。
void spin_unlock_irq(spinlock_t *lock) /激活本地中断,并释放自旋锁。
void spin_lock_irqsave(spinlock_t *lock, unsigned long flags)//保存中断状态,禁止本地中断,并获取自旋锁。
void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags)//将中断状态恢复到以前的状态,并且激活本地中断,释放自旋锁。

推荐使用的是spin_lock_irqsave和spin_unlock_irqrestore
下半部

void spin_lock_bh(spinlock_t *lock) //关闭下半部,并获取自旋锁。
void spin_unlock_bh(spinlock_t *lock) //打开下半部,并释放自旋锁。

注意事项

1、锁的持有时间不能太长,否则会降低系统性能
2、临界区不能使用休眠和阻塞
3、不能递归,否则会自己锁死

信号量

信号量就是指定可以访问的次数的锁,当达到次数后,就拿不到信号量,从而进入休眠;在临界区内可以休眠或阻塞
由于信号量会导致休眠,所以不能在中断中使用。另外由于会有线程的切换,有性能上的损耗,所以临界区或者共享资源持有时间短的情况下,不太适合使用信号量。

API

DEFINE_SEAMPHORE(name) //定义一个信号量,并且设置信号量的值为 1。

void sema_init(struct semaphore *sem, int val) //初始化信号量 sem,设置信号量值为 val。
void down(struct semaphore *sem)//获取信号量,因为会导致休眠,因此不能在中断中使用。
int down_trylock(struct semaphore *sem);//尝试获取信号量,如果能获取到信号量就获取,并且返回 0。如果不能就返回非 0,并且不会进入休眠。
int down_interruptible(struct semaphore *sem)//获取信号量,和 down 类似,只是使用 down 进入休眠状态的线程不能被信号打断。而使用此函数进入休眠以后是可以被信号打断的。
void up(struct semaphore *sem) //释放信号量

互斥锁

可以理解为访问次数为1的特殊信号量。互斥锁只能自己获取并自己释放,不能递归;

API

DEFINE_MUTEX(name) //定义并初始化一个 mutex 变量。
void mutex_init(mutex *lock) //初始化 mutex。
void mutex_lock(struct mutex *lock)//获取 mutex,也就是给 mutex 上锁。如果获取不到就进休眠。
void mutex_unlock(struct mutex *lock) //释放 mutex,也就给 mutex 解锁。
int mutex_trylock(struct mutex *lock)//尝试获取 mutex,如果成功就返回 1,如果失败就返回 0。
int mutex_is_locked(struct mutex *lock)//判断 mutex 是否被获取,如果是的话就返回1,否则返回 0。
int mutex_lock_interruptible(struct mutex *lock)//使用此函数获取信号量失败进入休眠以后可以被信号打断。

阻塞与非阻塞

阻塞IO

阻塞IO在设备不可用时会休眠,直到可用唤醒
非阻塞IO在设备不可用时会返回错误码,应用根据错误码可以选择重复读取
在open时选择以阻塞(O_BLOCK)或非阻塞(O_NONBLOCK)方式访问设备

fd = open("/dev/xxx_dev", O_RDWR | O_NONBLOCK); /* 非阻塞方式打开 */
ret = read(fd, &data, sizeof(data)); /* 读取数据 */

等待队列

创建一个队列管理线程,当条件满足时唤醒队列中的一个线程

1、初始化

//创建等待队列头
wait_queue_head_t q;
//初始化等待队列头
init_waitqueue_head(wait_queue_head_t *q);
//也可以用宏一次完成等待队列头的定义和初始化,这样等待队列头就是静态的,在加载时就创建好了
DECLARE_WAIT_QUEUE_HEAD(q);

2、等待队列项

每个访问设备的线程就是一个等待队列项,当设备不可用时就要将线程对应的等待队列项加入到等待队列里

wait_queue_t wq;
DECLARE_WAITQUEUE(name, tsk);

name 就是等待队列项的名字,tsk 表示这个等待队列项属于哪个任务(进程),一般设置为current , 在 Linux 内 核 中 current 相 当 于 一 个 全 局 变 量 , 表 示 当 前 进 程

3、将等待队列项加入等待队列

只有添加到等待队列头中以后进程才能进入休眠态

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

4、等待唤醒

void wake_up(wait_queue_head_t *q);
void wake_up_interruptible(wait_queue_head_t *q);

wake_up 函数可以唤醒处于 TASK_INTERRUPTIBLE 和 TASK_UNINTERRUPTIBLE 状态的进
程,而 wake_up_interruptible 函数只能唤醒处于 TASK_INTERRUPTIBLE 状态的进程

5、等待事件

除了主动唤醒以外,也可以设置等待队列等待某个事件,当这个事件满足以后就自动唤醒
等待队列中的进程

/*
等待以 wq 为等待队列头的等待队列被唤醒,前提是 condition 条件必须满足(为真),
否则一直阻塞。此函数会将进程设置为TASK_UNINTERRUPTIBLE 状态
*/
wait_event(wq, condition);

/*
功能和 wait_event 类似,但是此函数可以添加超时时间,以 jiffies 为单位。
此函数有返回值,如果返回 0 的话表示超时时间到,而且 condition
为假。为 1 的话表示 condition 为真,也就是条件满足了。
*/
wait_event_timeout(wq, condition, timeout);

/*
与 wait_event 函数类似,但是此函数将进程设置为 
TASK_INTERRUPTIBLE,就是可以被信号打断。
*/
wait_event_interruptible(wq, condition);

/*
与 wait_event_timeout 函数类似,此函数也将进
程设置为 TASK_INTERRUPTIBLE,可以被信号打断
*/
wait_event_interruptible_timeout(wq, condition, timeout);

轮询

poll、epoll、select可以用来处理非阻塞访问的轮询
当应用使用上面的三个函数的时候,驱动的poll函数就会执行

1、select

监控多个文件描述符的读写情况,单线程中默认监视的最大1024个文件描述符,可以在内核中改大

int select( int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, 
            struct timeval *timeout);

nfds:所要监视的这三类文件描述集合中,最大文件描述符加 1。
readfds、writefds 和 exceptfds:要监控的文件描述符集合 fd_set ,按照读、写、异常进行分别监控
返回值 0:超时 -1:错误 大于0:文件描述集中有事件发生

fd_set可以用下列宏进行操作

void FD_ZERO(fd_set *set)
void FD_SET(int fd, fd_set *set)
void FD_CLR(int fd, fd_set *set)
int FD_ISSET(int fd, fd_set *set)

使用时先用FD_ZERO将文件描述符集合清零,之后用FD_SET将要监视的文件描述符加入到集合中,有事件返回后用FD_ISSET进行查看是哪个文件描述符有事件发生,不需要监视时用FD_CLR将文件描述符从集合中拿出

2、poll

同样可以监视多个文件描述符数量,但是没有个数的限制;

int poll(struct pollfd *fds,  nfds_t nfds,  int timeout);

fds:struct pollfd的数组指针,表示多个文件描述符和对应要监控的事件和返回的事件

struct pollfd {
    int fd; /* 文件描述符 */
    short events; /* 请求的事件 */
    short revents; /* 返回的事件 */
};

nfds:监视的文件描述符数量
返回值:0超时 -1:异常 大于0:有事件发生
事件类型:

POLLIN //有数据可以读取。
POLLPRI //有紧急的数据需要读取。
POLLOUT //可以写数据。
POLLERR //指定的文件描述符发生错误。
POLLHUP //指定的文件描述符挂起。
POLLNVAL //无效的请求。
POLLRDNORM //等同于 POLLIN

3、epoll

epoll相比select和poll更高效,内部采用了红黑树进行管理,并有边沿触发和电平触发两种模式,默认电平触发
使用流程
1、创建epoll句柄

int epoll_create(int size);

size:没有特殊含义,大于0即可
返回值:epoll句柄
2、用epoll_ctl将文件描述符和要监视的事件添加到句柄中

int epoll_ctl(int epfd,  int op,  int fd, struct epoll_event *event);

epfd:epoll句柄
op:对epoll句柄采用的操作类型,有以下几种

EPOLL_CTL_ADD //向 epfd 添加文件参数 fd 表示的描述符。
EPOLL_CTL_MOD //修改参数 fd 的 event 事件。
EPOLL_CTL_DEL //从 epfd 中删除 fd 描述符。

fd:监视的文件描述符
event:事件类型

struct epoll_event {
uint32_t events; /* epoll 事件 */
epoll_data_t data; /* 用户数据 */
};

事件类型可分为以下

EPOLLIN         //有数据可以读取。
EPOLLOUT        //可以写数据。
EPOLLPRI        //有紧急的数据需要读取。
EPOLLERR        //指定的文件描述符发生错误。
EPOLLHUP        //指定的文件描述符挂起。
EPOLLET         //设置 epoll 为边沿触发,默认触发模式为水平触发。
EPOLLONESHOT    //一次性的监视,当监视完成以后还需要再次监视某个 fd,那么就需要将fd 重新添加到 epoll 里面。

3、用epoll_wait进行等待,此等待会引起休眠

int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

有事件发生时会唤醒,事件类型会被写到events中,events要是一个数组,数量为要监视的文件描述符大小,maxevents也是数组大小的意思
返回值 0:超时 -1:异常 大于0:有事件发生

驱动poll
当应用程序调用 select 或 poll 函数来对驱动程序进行非阻塞访问时,驱动程序file_operations 操作集中的 poll 函数就会执行

unsigned int (*poll) (struct file *filp, struct poll_table_struct *wait)

filp:文件描述符
wait:由系统调用传入,poll_wait函数需要用到
void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
poll_wait的作用是将线程添加到poll_table中,
wait_address:要添加到poll_table的等待队列头,此等待队列头用来将线程唤醒返回事件
p:poll_table,也就是poll的wait参数

异步通知

当需要由驱动告诉应用可以访问的时候使用,异步使用信号的通知方式
1、应用层通过设置进程启用异步通知,

/* 设置信号 SIGIO 的处理函数 */
signal(SIGIO, sigio_signal_func);

fcntl(fd, F_SETOWN, getpid()); /* 将当前进程的进程号告诉给内核 */
flags = fcntl(fd, F_GETFD); /* 获取当前的进程状态 */
fcntl(fd, F_SETFL, flags | FASYNC);/* 设置进程启用异步通知功能 */ 

2、驱动的file_operations中的fasync函数就会调用

struct fasync_struct *async_queue;
static int xxx_fasync(int fd, struct file *filp, int on)
{
    struct xxx_dev *dev = (xxx_dev)filp->private_data;
    
    if (fasync_helper(fd, filp, on, &dev->async_queue) < 0)
    return -EIO;
    return 0;
}

3、驱动有事件通知应用时,用kill_fasync进行通知

void kill_fasync(struct fasync_struct **fp, int sig, int band)

fp:要操作的 fasync_struct。
sig:要发送的信号。
band:可读时设置为 POLL_IN,可写时设置为 POLL_OUT
4、应用的用signal函数关联的信号函数sigio_signal_func就会被调用,从而达到异步通知的目的
5、不需要异步通知时

static int xxx_release(struct inode *inode, struct file *filp)
{
    return xxx_fasync(-1, filp, 0); /* 删除异步通知 */
}
  • 11
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值