嵌入式Linux驱动开发基础总结(下篇)

电子说

1.2w人已加入

描述

14, 字符设备驱动程序设计基础

主设备号和次设备号(二者一起为设备号): 一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。 linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

typedef u_long dev_t;

在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。

可以使用下列宏从dev_t中获得主次设备号:也可以使用下列宏通过主次设备号生成dev_t:

MAJOR(dev_tdev);MKDEV(intmajor,intminor);MINOR(dev_tdev);

分配设备号(两种方法): (1)静态申请:

int register_chrdev_region(dev_t from,unsigned count,const char *name);

(2)动态分配:

int alloc_chrdev_region(dev_t *dev,unsigned baseminor,unsigned count,const char *name);

注销设备号:

void unregister_chrdev_region(dev_t from,unsigned count);

创建设备文件: 利用cat/proc/devices查看申请到的设备名,设备号。 (1)使用mknod手工创建:mknod filename type major minor (2)自动创建; 利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

15, 字符设备驱动程序设计

设备注册: 字符设备的注册分为三个步骤: (1)分配

cdev:struct cdev *cdev_alloc(void);

(2)初始化

cdev:void cdev_init(struct cdev *cdev,const struct file_operations *fops);

(3)添加

cdev:int cdev_add(struct cdev *p,dev_t dev,unsigned count)

设备操作的实现: file_operations函数集的实现。

struct file_operations xxx_ops={.owner=THIS_MODULE,.llseek=xxx_llseek,.read=xxx_read,.write=xxx_write,.ioctl=xxx_ioctl,.open=xxx_open,.release=xxx_release,

};

特别注意:驱动程序应用程序的数据交换: 驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

unsigned long copy_to_user(void__user *to,const void *from,unsigned long n);unsigned long copy_from_user(void *to,constvoid __user *from,unsigned long n);

put_user(local,user);

get_user(local,user);

设备注销:

void cdev_del(struct cdev *p);

16,ioctl函数说明

ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:

int ioctl(int fd,ind cmd,…);

其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,后面的省略号是一些补充参数,有或没有是和cmd的意义相关的。 

ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。

命令的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。 

所以在Linux核心中是这样定义一个命令码的:

这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以LinuxKernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。 点击(此处)折叠或打开

/*used to create numbers*/

#define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)

#define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))

#define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

#define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))

#defin e_IOR_BAD(type,nr,size)    _IOC(_IOC_READ,(type),(nr),sizeof(size))

#define _IOW_BAD(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))

#define _IOWR_BAD(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))

#define _IOC(dir,type,nr,size)\

(((dir)<<_IOC_DIRSHIFT)|\

((type)<<_IOC_TYPESHIFT)|\

((nr)<<_IOC_NRSHIFT)|\

((size)<<_IOC_SIZESHIFT))

17,文件私有数据

大多数linux的驱动工程师都将文件私有数据private_data指向设备结构体,read等个函数通过调用private_data来访问设备结构体。这样做的目的是为了区分子设备,如果一个驱动有两个子设备(次设备号分别为0和1),那么使用private_data就很方便。 

这里有一个函数要提出来:

container_of(ptr,type,member)//通过结构体成员的指针找到对应结构体的的指针

其定义如下:

/**

*container_of-castamemberofastructureouttothecontainingstructure

*@ptr:    thepointertothemember.

*@type:    thetypeofthecontainerstructthisisembeddedin.

*@member:    thenameofthememberwithinthestruct.

*

*/

#define container_of(ptr,type,member)({            \

const typeof(((type*)0)->member)*__mptr=(ptr);    \

(type*)((char*)__mptr-offsetof(type,member));})

18,字符设备驱动的结构

可以概括如下图:  字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()、write()、ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。

19, 自旋锁与信号量

为了避免并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别。 

Linux使用的同步机制可以说从2.0到2.6以来不断发展完善。从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随Linux从单处理器到对称多处理器的过度;伴随着从非抢占内核到抢占内核的过度。锁机制越来越有效,也越来越复杂。目前来说内核中原子操作多用来做计数使用,其它情况最常用的是两种锁以及它们的变种:一个是自旋锁,另一个是信号量。

自旋锁 自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。 

自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

自旋锁的基本形式如下:

spin_lock(&mr_lock);//临界区spin_unlock(&mr_lock);

·

信号量 Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。 

信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。 

信号量基本使用形式为:

static DECLARE_MUTEX(mr_sem);//声明互斥信号量if(down_interruptible(&mr_sem))//可被中断的睡眠,当信号来到,睡眠的任务被唤醒//临界区up(&mr_sem);

信号量和自旋锁区别 从严格意义上说,信号量和自旋锁属于不同层次的互斥手段,前者的实现有赖于后者,在信号量本身的实现上,为了保证信号量结构存取的原子性,在多CPU中需要自旋锁来互斥。 信号量是进程级的。用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间比较长时,用信号量才是较好的选择。 

当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间,但是CPU得不到自旋锁会在那里空转直到执行单元锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率 

由此,可以总结出自旋锁和信号量选用的3个原则: 

1:当锁不能获取到时,使用信号量的开销就是进程上线文切换的时间Tc,使用自旋锁的开销就是等待自旋锁(由临界区执行的时间决定)Ts,如果Ts比较小时,应使用自旋锁比较好,如果Ts比较大,应使用信号量。 

2:信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁绝对要避免用来保护包含这样的代码的临界区,因为阻塞意味着要进行进程间的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。 

3:信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁,当然,如果一定要是要那个信号量,则只能通过down_trylock()方式进行,不能获得就立即返回以避免阻塞 

自旋锁VS信号量 需求建议的加锁方法 低开销加锁优先使用自旋锁 短期锁定优先使用自旋锁 长期加锁优先使用信号量 中断上下文中加锁使用自旋锁 持有锁是需要睡眠、调度使用信号量

20, 阻塞与非阻塞I/O

一个驱动当它无法立刻满足请求应当如何响应?一个对 read 的调用可能当没有数据时到来,而以后会期待更多的数据;或者一个进程可能试图写,但是你的设备没有准备好接受数据,因为你的输出缓冲满了。调用进程往往不关心这种问题,程序员只希望调用 read 或 write 并且使调用返回,在必要的工作已完成后,你的驱动应当(缺省地)阻塞进程,使它进入睡眠直到请求可继续。 

阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。 

一个典型的能同时处理阻塞与非阻塞的globalfifo读函数如下:

/*globalfifo读函数*/

static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count,

loff_t *ppos)

{

int ret;

struct globalfifo_dev *dev = filp->private_data;

DECLARE_WAITQUEUE(wait, current);

down(&dev->sem); /* 获得信号量 */

add_wait_queue(&dev->r_wait, &wait); /* 进入读等待队列头 */

/* 等待FIFO非空 */

if (dev->current_len == 0) {

if (filp->f_flags &O_NONBLOCK) {

ret = - EAGAIN;

goto out;

}

__set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态为睡眠 */

up(&dev->sem);

schedule(); /* 调度其他进程执行 */

if (signal_pending(current)) {

/* 如果是因为信号唤醒 */

ret = - ERESTARTSYS;

goto out2;

}

down(&dev->sem);

}

/* 拷贝到用户空间 */

if (count > dev->current_len)

count = dev->current_len;

if (copy_to_user(buf, dev->mem, count)) {

ret = - EFAULT;

goto out;

} else {

memcpy(dev->mem, dev->mem + count, dev->current_len - count); /* fifo数据前移 */

dev->current_len -= count; /* 有效数据长度减少 */

printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);

wake_up_interruptible(&dev->w_wait); /* 唤醒写等待队列 */

ret = count;

}

out:

up(&dev->sem); /* 释放信号量 */

out2:

remove_wait_queue(&dev->w_wait, &wait); /* 从附属的等待队列头移除 */

set_current_state(TASK_RUNNING);

return ret;

}

21, poll方法

使用非阻塞I/O的应用程序通常会使用select()和poll()系统调用查询是否可对设备进行无阻塞的访问。select()和poll()系统调用最终会引发设备驱动中的poll()函数被执行。 这个方法由下列的原型:

unsigned int (*poll) (struct file *filp, poll_table *wait);

这个驱动方法被调用, 无论何时用户空间程序进行一个 poll, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符. 这个设备方法负责这 2 步:

1. 对可能引起设备文件状态变化的等待队列,调用poll_wait()函数,将对应的等待队列头添加到poll_table.

2. 返回一个位掩码, 描述可能不必阻塞就立刻进行的操作.

poll_table结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 中声明, 这个文件必须被驱动源码包含. 驱动编写者不必要知道所有它内容并且必须作为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它, 并且可改变 poll 操作状态. 驱动增加一个等待队列到poll_table结构通过调用函数 poll_wait:

void poll_wait (struct file *, wait_queue_head_t *, poll_table *);

poll 方法的第 2 个任务是返回位掩码, 它描述哪个操作可马上被实现; 这也是直接的. 例如, 如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(通过 定义)用来指示可能的操作: POLLIN:如果设备可被不阻塞地读, 这个位必须设置. POLLRDNORM:这个位必须设置, 如果”正常”数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ). POLLOUT:这个位在返回值中设置, 如果设备可被写入而不阻塞. …… poll的一个典型模板如下:

static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)

{

unsigned int mask = 0;

struct globalfifo_dev *dev = filp->private_data; /*获得设备结构体指针*/

down(&dev->sem);

poll_wait(filp, &dev->r_wait, wait);

poll_wait(filp, &dev->w_wait, wait);

/*fifo非空*/

if (dev->current_len != 0) {

mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/

}

/*fifo非满*/

if (dev->current_len != GLOBALFIFO_SIZE) {

mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/

}

up(&dev->sem);

return mask;

}

应用程序如何去使用这个poll呢?一般用select()来实现,其原型为:

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

其中,readfds, writefds, exceptfds,分别是被select()监视的读、写和异常处理的文件描述符集合。numfds是需要检查的号码最高的文件描述符加1。 

以下是一个具体的例子:

/*======================================================================

A test program in userspace

This example is to introduce the ways to use "select"

and driver poll

The initial developer of the original code is Baohua Song

. All Rights Reserved.

======================================================================*/#include #include #include #include #include #include

#define FIFO_CLEAR 0x1#define BUFFER_LEN 20

main()

{

int fd, num;

char rd_ch[BUFFER_LEN];

fd_set rfds,wfds;

/*以非阻塞方式打开/dev/globalmem设备文件*/

fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);

if (fd != - 1)

{

/*FIFO清0*/

if (ioctl(fd, FIFO_CLEAR, 0) < 0)

{

printf("ioctl command failed\n");

}

while (1)

{

FD_ZERO(&rfds);// 清除一个文件描述符集rfds

FD_ZERO(&wfds);

FD_SET(fd, &rfds);// 将一个文件描述符fd,加入到文件描述符集rfds中

FD_SET(fd, &wfds);

select(fd + 1, &rfds, &wfds, NULL, NULL);

/*数据可获得*/

if (FD_ISSET(fd, &rfds)) //判断文件描述符fd是否被置位

{

printf("Poll monitor:can be read\n");

}

/*数据可写入*/

if (FD_ISSET(fd, &wfds))

{

printf("Poll monitor:can be written\n");

}

}

}

else

{

printf("Device open failure\n");

}

}

其中: FD_ZERO(fd_set *set); //清除一个文件描述符集set FD_SET(int fd, fd_set *set); //将一个文件描述符fd,加入到文件描述符集set中 FD_CLEAR(int fd, fd_set *set); //将一个文件描述符fd,从文件描述符集set中清除 FD_ISSET(int fd, fd_set *set); //判断文件描述符fd是否被置位。

22,并发与竞态介绍

Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态,在当今的Linux内核中,支持SMP与内核抢占的环境下,更是充满了并发与竞态。幸运的是,Linux 提供了多钟解决竞态问题的方式,这些方式适合不同的应用场景。例如:中断屏蔽、原子操作、自旋锁、信号量等等并发控制机制。 

并发与竞态的概念 并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。 

临界区概念是为解决竞态条件问题而产生的,一个临界区是一个不允许多路访问的受保护的代码,这段代码可以操纵共享数据或共享服务。临界区操纵坚持互斥锁原则(当一个线程处于临界区中,其他所有线程都不能进入临界区)。然而,临界区中需要解决的一个问题是死锁。

23, 中断屏蔽

在单CPU 范围内避免竞态的一种简单而省事的方法是进入临界区之前屏蔽系统的中断。CPU 一般都具有屏蔽中断和打开中断的功能,这个功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,有效的防止了某些竞态条件的发送,总之,中断屏蔽将使得中断与进程之间的并发不再发生。 

中断屏蔽的使用方法:

local_irq_disable() /屏蔽本地CPU 中断/ 

…..

critical section /临界区受保护的数据/ 

…..

local_irq_enable() /打开本地CPU 中断/ 

由于Linux 的异步I/O、进程调度等很多重要操作都依赖于中断,中断对内核的运行非常重要,在屏蔽中断期间的所有中断都无法得到处理,因此长时间屏蔽中断是非常危险的,有可能造成数据的丢失,甚至系统崩溃的后果。这就要求在屏蔽了中断后,当前的内核执行路径要尽快地执行完临界区代码。 

与local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作外,还保存当前CPU 的中断状态位信息;与local_irq_enable()不同的是,local_irq_restore(flags) 除了打开中断的操作外,还恢复了CPU 被打断前的中断状态位信息。

24, 原子操作

原子操作指的是在执行过程中不会被别的代码路径所中断的操作,Linux 内核提供了两类原子操作——位原子操作和整型原子操作。它们的共同点是在任何情况下都是原子的,内核代码可以安全地调用它们而不被打断。然而,位和整型变量原子操作都依赖于底层CPU 的原子操作来实现,因此这些函数的实现都与 CPU 架构密切相关。 

1 整型原子操作 1)、设置原子变量的值

void atomic_set(atomic v,int i); /设置原子变量的值为 i */ 

atomic_t v = ATOMIC_INIT(0); /定义原子变量 v 并初始化为 0 / 

2)、获取原子变量的值

int atomic_read(atomic_t v) /返回原子变量 v 的当前值*/

3)、原子变量加/减

void atomic_add(int i,atomic_t v) /原子变量增加 i */

void atomic_sub(int i,atomic_t v) /原子变量减少 i */

4)、原子变量自增/自减

void atomic_inc(atomic_t v) /原子变量增加 1 */

void atomic_dec(atomic_t v) /原子变量减少 1 */

5)、操作并测试

int atomic_inc_and_test(atomic_t *v);

int atomic_dec_and_test(atomic_t *v);

int atomic_sub_and_test(int i, atomic_t *v);

上述操作对原子变量执行自增、自减和减操作后测试其是否为 0 ,若为 0 返回true,否则返回false。注意:没有atomic_add_and_test(int i, atomic_t *v)。 

6)、操作并返回

int atomic_add_return(int i, atomic_t *v);

int atomic_sub_return(int i, atomic_t *v);

int atomic_inc_return(atomic_t *v);

int atomic_dec_return(atomic_t *v);

上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。 

2 位原子操作 1)、设置位

void set_bit(nr,void addr);/设置addr 指向的数据项的第 nr 位为1 */

2)、清除位

void clear_bit(nr,void addr)/设置addr 指向的数据项的第 nr 位为0 */

3)、取反位

void change_bit(nr,void addr); /对addr 指向的数据项的第 nr 位取反操作*/

4)、测试位

test_bit(nr,void addr);/返回addr 指向的数据项的第 nr位*/

5)、测试并操作位

int test_and_set_bit(nr, void *addr);

int test_and_clear_bit(nr,void *addr);

int test_amd_change_bit(nr,void *addr);

25, 自旋锁

自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了获得一个自旋锁,在某CPU 上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,则程序将在一个小的循环里面重复这个“测试并设置” 操作,即进行所谓的“自旋”。 

理解自旋锁最简单的方法是把它当做一个变量看待,该变量把一个临界区标记为“我在这运行了,你们都稍等一会”,或者标记为“我当前不在运行,可以被使用”。 

Linux中与自旋锁相关操作有: 1)、定义自旋锁

spinlock_t my_lock;

2)、初始化自旋锁

spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /静态初始化自旋锁/

void spin_lock_init(spinlock_t lock); /动态初始化自旋锁*/

3)、获取自旋锁

/若获得锁立刻返回真,否则自旋在那里直到该锁保持者释放/

void spin_lock(spinlock_t *lock);

/若获得锁立刻返回真,否则立刻返回假,并不会自旋等待/

void spin_trylock(spinlock_t *lock)

4)、释放自旋锁

void spin_unlock(spinlock_t *lock)

自旋锁的一般用法:

spinlock_t lock; /定义一个自旋锁/

spin_lock_init(&lock); /动态初始化一个自旋锁/

……

spin_lock(&lock); /获取自旋锁,保护临界区/

……./临界区/

spin_unlock(&lock); /解锁/

自旋锁主要针对SMP 或单CPU 但内核可抢占的情况,对于单CPU 且内核不支持抢占的系统,自旋锁退化为空操作。尽管用了自旋锁可以保证临界区不受别的CPU和本地CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响,为了防止这种影响,就需要用到自旋锁的衍生。 

获取自旋锁的衍生函数:

void spin_lock_irq(spinlock_t lock); /获取自旋锁之前禁止中断*/ void spin_lock_irqsave(spinlock_t lock, unsigned long flags);/获取自旋锁之前禁止中断,并且将先前的中断状态保存在flags 中*/ void spin_lock_bh(spinlock_t lock); /在获取锁之前禁止软中断,但不禁止硬件中断*/

释放自旋锁的衍生函数:

void spin_unlock_irq(spinlock_t *lock) 

void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags);

void spin_unlock_bh(spinlock_t *lock);

解锁的时候注意要一一对应去解锁。 自旋锁注意点: (1)自旋锁实际上是忙等待,因此,只有占用锁的时间极短的情况下,使用自旋锁才是合理的。 (2)自旋锁可能导致系统死锁。 (3)自旋锁锁定期间不能调用可能引起调度的函数。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函数。 (4)拥有自旋锁的代码是不能休眠的。

26, 读写自旋锁

它允许多个读进程并发执行,但是只允许一个写进程执行临界区代码,而且读写也是不能同时进行的。 1)、定义和初始化读写自旋锁

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */

rwlock_t my_rwlock;

rwlock_init(&my_rwlock); /* 动态初始化 */

2)、读锁定

void read_lock(rwlock_t *lock); void read_lock_irqsave(rwlock_t *lock, unsigned long flags); void read_lock_irq(rwlock_t *lock); void read_lock_bh(rwlock_t *lock);

3)、读解锁

void read_unlock(rwlock_t *lock); void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void read_unlock_irq(rwlock_t *lock); void read_unlock_bh(rwlock_t *lock);

在对共享资源进行读取之前,应该先调用读锁定函数,完成之后调用读解锁函数。 

4)、写锁定

void write_lock(rwlock_t *lock); void write_lock_irqsave(rwlock_t *lock, unsigned long flags); void write_lock_irq(rwlock_t *lock); void write_lock_bh(rwlock_t *lock); void write_trylock(rwlock_t *lock);

5)、写解锁

void write_unlock(rwlock_t *lock); void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); void write_unlock_irq(rwlock_t *lock); void write_unlock_bh(rwlock_t *lock);

在对共享资源进行写之前,应该先调用写锁定函数,完成之后应调用写解锁函数。

读写自旋锁的一般用法:

rwlock_t lock; /定义一个读写自旋锁 rwlock/

rwlock_init(&lock); /初始化/

read_lock(&lock); /读取前先获取锁/

…../临界区资源/

read_unlock(&lock); /读完后解锁/

write_lock_irqsave(&lock, flags); /写前先获取锁/

…../临界区资源/

write_unlock_irqrestore(&lock,flags); /写完后解锁/

27, 顺序锁(sequence lock)

顺序锁是对读写锁的一种优化,读执行单元在写执行单元对被顺序锁保护的资源进行写操作时仍然可以继续读,而不必等地写执行单元完成写操作,写执行单元也不必等待所有读执行单元完成读操作才进去写操作。但是,写执行单元与写执行单元依然是互斥的。并且,在读执行单元读操作期间,写执行单元已经发生了写操作,那么读执行单元必须进行重读操作,以便确保读取的数据是完整的,这种锁对于读写同时进行概率比较小的情况,性能是非常好的。 

顺序锁有个限制,它必须要求被保护的共享资源不包含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,就会导致oops。 

1)、初始化顺序锁

seqlock_t lock1 = SEQLOCK_UNLOCKED; /静态初始化/ 

seqlock lock2; /动态初始化/ 

seqlock_init(&lock2)

2)、获取顺序锁

void write_seqlock(seqlock_t *s1);

void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags)

void write_seqlock_irq(seqlock_t *lock);

void write_seqlock_bh(seqlock_t *lock); int write_tryseqlock(seqlock_t *s1);

3)、释放顺序锁

void write_sequnlock(seqlock_t *s1);

void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags)

void write_sequnlock_irq(seqlock_t *lock);

void write_sequnlock_bh(seqlock_t *lock);

写执行单元使用顺序锁的模式如下:

write_seqlock(&seqlock_a); /写操作代码/ 

……..

write_sequnlock(&seqlock_a);

4)、读开始

unsigned read_seqbegin(const seqlock_t *s1); unsigned read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);

5)、重读

int read_seqretry(const seqlock_t *s1, unsigned iv); int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,unsigned long flags);

读执行单元使用顺序锁的模式如下:

unsigned int seq; do{

seq = read_seqbegin(&seqlock_a);

/读操作代码/

…….

}while (read_seqretry(&seqlock_a, seq));

28, 信号量

信号量的使用 信号量(semaphore)是用于保护临界区的一种最常用的办法,它的使用方法与自旋锁是类似的,但是,与自旋锁不同的是,当获取不到信号量的时候,进程不会自旋而是进入睡眠的等待状态。 1)、定义信号量

struct semaphore sem;

2)、初始化信号量

void sema_init(struct semaphore sem, int val); /初始化信号量的值为 val */

更常用的是下面这二个宏:

#define init_MUTEX(sem) sema_init(sem, 1) #define init_MUTEX_LOCKED(sem) sem_init(sem, 0)

然而,下面这两个宏是定义并初始化信号量的“快捷方式”

DECLARE_MUTEX(name) /一个称为name信号量变量被初始化为 1 /

DECLARE_MUTEX_LOCKED(name) /一个称为name信号量变量被初始化为 0 /

3)、获得信号量

/该函数用于获取信号量,若获取不成功则进入不可中断的睡眠状态/ void down(struct semaphore *sem);

/该函数用于获取信号量,若获取不成功则进入可中断的睡眠状态/ void down_interruptible(struct semaphore *sem);

/该函数用于获取信号量,若获取不成功立刻返回 -EBUSY/ int down_trylock(struct sempahore *sem);

4)、释放信号量

void up(struct semaphore sem); /释放信号量 sem ,并唤醒等待者*/

信号量的一般用法:

DECLARE_MUTEX(mount_sem); /定义一个信号量mount_sem,并初始化为 1 / 

down(&mount_sem); /* 获取信号量,保护临界区*/ 

…..

critical section /临界区/ 

…..

up(&mount_sem); /释放信号量/ 

29, 读写信号量

读写信号量可能引起进程阻塞,但是它允许多个读执行单元同时访问共享资源,但最多只能有一个写执行单元。 1)、定义和初始化读写信号量

struct rw_semaphore my_rws; /定义读写信号量/

void init_rwsem(struct rw_semaphore sem); /初始化读写信号量*/

2)、读信号量获取

void down_read(struct rw_semaphore *sem);

int down_read_trylock(struct rw_semaphore *sem);

3)、读信号量释放

void up_read(struct rw_semaphore *sem);

4)、写信号量获取

void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

5)、写信号量释放

void up_write(struct rw_semaphore *sem);

30, completion

完成量(completion)用于一个执行单元等待另外一个执行单元执行完某事。 1)、定义完成量

struct completion my_completion;

2)、初始化完成量

init_completion(&my_completion);

3)、定义并初始化的“快捷方式”

DECLARE_COMPLETION(my_completion)

4)、等待完成量

void wait_for_completion(struct completion c); /等待一个 completion 被唤醒*/

5)、唤醒完成量

void complete(struct completion c); /只唤醒一个等待执行单元*/

void complete(struct completion c); /唤醒全部等待执行单元*/

31, 自旋锁VS信号量

信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发送进程上下文切换,当前进程进入睡眠状态,CPU 将运行其他进程。鉴于开销比较大,只有当进程资源时间较长时,选用信号量才是比较合适的选择。然而,当所要保护的临界区访问时间比较短时,用自旋锁是比较方便的。 

总结: 解决并发与竞态的方法有(按本文顺序): 

(1)中断屏蔽 (2)原子操作(包括位和整型原子) (3)自旋锁 (4)读写自旋锁 (5)顺序锁(读写自旋锁的进化) (6)信号量 (7)读写信号量 (8)完成量 

其中,中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。自旋锁会导致死循环,锁定期间内不允许阻塞,因此要求锁定的临界区小;信号量允许临界区阻塞,可以适用于临界区大的情况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁 信号量,它们允许多个执行单元对共享资源的并发读。

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分