初级驱动day5

一. 多路复用

:::info
使用poll机制时,驱动程序的核心就是提供对应的drv_poll函数。
:::
poll机制分析> 总的来说,Poll机制会判断fds中的文件是否可读,如果可读则会立即返回,返回的值就是可读fd的数量,如果不可读,那么就进程就会休眠timeout这么长的时间,然后再来判断是否有文件可读,如果有,返回fd的数量,如果没有,则返回0.

在内核中大致上实现过程:
当应用程序调用poll函数的时候,会调用到系统调用sys_poll函数,该函数最终调用do_poll函数,do_poll函数中有一个死循 环,在里面又会利用do_pollfd函数去调用驱动中的poll函数(fds中每个成员的字符驱动程序都会被扫描到),驱动程序中的Poll函数的工作 有两个,一个就是调用poll_wait 函数,把进程挂到等待队列中去(这个是必须的,你要睡眠,必须要在一个等待队列上面,否则到哪里去唤醒你呢??),另一个是确定相关的fd是否有内容可 读,如果可读,就返回1,否则返回0,如果返回1 ,do_poll函数中的count++, 然后 do_poll函数然后判断三个条件(if (count ||!timeout || signal_pending(current)))如果成立就直接跳出,如果不成立,就睡眠timeout个jiffes这么长的时间(调用schedule_timeout实现睡眠),如果在这段时间内没有其他进程去唤醒它,那么第二次执行判断的时候就会跳出死循环。如果在这段时间内有其他进程唤醒它,那么也可以跳出死循环返回(例如我们可以利用中断处理函数去唤醒它,这样的话一有数据可读,就可以让它立即返回)。所有的系统调用,基于都可以在它的名字前加上“sys_”前缀,这就是它在内核中对应的函数。比如系统调用open、read、write、poll,与之对应的内核函数为:sys_open、sys_read、sys_write、sys_poll。

:::info
在drv_poll函数中要做2件事:
① 把当前线程挂入队列wq:poll_wait
drv_poll要把自己这个线程挂入等待队列wq中;假设不放入队列里,那以后发生中断时,中断服务程序去哪里找到你嘛?
② 返回设备状态:
线程被唤醒的原因有2:中断发生了去队列wq中把它唤醒,超时时间到了内核把它唤醒
APP调用poll函数时,有可能是查询“有没有数据可以读”:POLLIN,也有可能是查询“你有没有空间给我写数据”:POLLOUT。
所以drv_poll要返回自己的当前状态:(POLLIN | P OLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
POLLRDNORM等同于POLLIN,为了兼容某些APP把它们一起返回。
POLLWRNORM等同于POLLOUT ,为了兼容某些APP把它们一起返回。

APP调用poll后,很有可能会休眠。对应的,在按键驱动的中断服务程序中,也要有唤醒操作。

在调用poll函数时,要指明:
① 你要监测哪一个文件:哪一个fd
② 你想监测这个文件的哪种事件:是POLLIN、还是POLLOUT
最后,在poll函数返回时,要判断状态。
:::

//驱动程序
unsigned int button_drv_poll(struct file * filp, struct poll_table_struct * poll_table)
{
    unsigned int ret = 0;
    //把当前的等待队列头注册到轮询表
    poll_wait(filp, &fs210_button->wait, poll_table);
    if(fs210_button->have_data)
    ret |= POLLIN;
    return ret;
}
	
//应用程序
fds[0].fd = fd;
fds[0].events = POLLIN;

fds[1].fd = 0;
fds[1].events = POLLIN;

while(1)
{
    int ret;
    ret = poll(fds, sizeof(fds)/sizeof(fds[0]), -1);
    if(ret > 0)
    {
   		if(fds[1].revents & POLLIN)
        {
        	//有标准输入数据
        }
        if(fds[0].revents & POLLIN)
        {
       		read(fd, &kdata, sizeof(struct key_data));
        	//有按键数据
        }
    }
}

二. 异步通知

:::warning
APP要做什么事?想想这几个问题:
① 内核里有那么多驱动,你想让哪一个驱动给你发SIGIO信号?
APP要打开驱动程序的设备节点。
② 驱动程序怎么知道要发信号给你而不是别人?
APP要把自己的进程ID告诉驱动程序。
③ APP有时候想收到信号,有时候又不想收到信号:
应该可以把APP的意愿告诉驱动。

驱动程序要做什么?发信号。
① APP设置进程ID时,驱动程序要记录下进程ID;
② APP还要使能驱动程序的异步通知功能,
APP打开驱动程序时,内核会创建对应的file结构体,file中有f_flags;
f_flags中有一个FASYNC位,它被设置为1时表示使能异步通知功能。
当f_flags中的FASYNC位发生变化时,驱动程序的fasync函数被调用。
③ 发生中断时,有数据时,驱动程序调用内核辅助函数发信号。
这个辅助函数名为kill_fasync

设置Flag里面的FASYNC位为1:当FASYNC位发生变化时,会导致驱动程序的fasync被调用;
调用faync_helper,它会根据FAYSNC的值决定是否设置button_async->fa_file=驱动文件filp:
驱动文件filp结构体里面含有之前设置的PID
:::

//应用程序
void button_sighandler(int signal)
{
	//信号处理函数
}
//注册信号
signal(SIGIO, button_sighandler);
fd = open("/dev/button_drv",O_RDWR);
if(fd < 0)
{
	perror("open");
	exit(1);
}

fcntl(fd, F_SETOWN, getpid());
Oflags = fcntl(fd, F_GETFL);
fcntl(fd, F_SETFL, Oflags | FASYNC);
	
//驱动程序
int button_drv_fasync(int fd, struct file * filp, int on)
{
	return fasync_helper(fd, filp, on, &fs210_button->fasync);
}

//发送信号
kill_fasync(&fs210_button->fasync, SIGIO, POLL_IN);

三. 定时器

1. 内核函数
:::info
编译内核时,可以在内核源码根目录下用“ls -a”看到一个隐藏文件,它就是内核配置文件。打开后可以看到如下这项:CONFIG_HZ=100
这表示内核每秒中会发生100次系统滴答中断(tick),这就像人类的心跳一样,这是Linux系统的心跳。每发生一次tick中断,全局变量jiffies就会累加1。
CONFIG_HZ=100表示每个滴答是10ms。
定时器的时间就是基于jiffies的,我们修改超时时间时,一般使用这2种方法:
① 在add_timer之前,直接修改:
timer.expires = jiffies + xxx; // xxx表示多少个滴答后超时,也就是xxx10ms
timer.expires = jiffies + 2
HZ; // HZ等于CONFIG_HZ,2HZ就相当于2秒
② 在add_timer之后,使用mod_timer修改:
mod_timer(&timer, jiffies + xxx); // xxx表示多少个滴答后超时,也就是xxx
10ms
mod_timer(&timer, jiffies + 2HZ); // HZ等于CONFIG_HZ,2HZ就相当于2秒
:::

//初始化定时器
init_timer(&stm32mp157_button->timer);
//设置定时的处理函数
stm32mp157_button->timer.function = button_irq_function;
//stm32mp157_button->timer.expires = 0;
//把定时器添加到内核
add_timer(&stm32mp157_button->timer);
//设置定时器触发函数(jiffies: 滴答定时器每触发一次中断,jiffies就会加1, HZ表示滴答定时器触发中断的频率)
mod_timer(&stm32mp157_button->timer, jiffies + HZ/100);

四. mmap函数

:::info
应用程序和驱动程序之间传递数据时,可以通过read、write函数进行。这涉及在用户态buffer和内核态buffer之间传数据
应用程序不能直接读写驱动程序中的buffer,需要在用户态buffer和内核态buffer之间进行一次数据拷贝。这种方式在数据量比较小时没什么问题;但是数据量比较大时效率就太低了。比如更新LCD显示时,如果每次都让APP传递一帧数据给内核,假设LCD采用102460032bpp的格式,一帧数据就有102460032/8=2.3MB左右,这无法忍受。
改进的方法就是让程序可以直接读写驱动程序中的buffer,这可以通过mmap实现(memory map),把内核的buffer映射到用户态,让APP在用户态直接读写。
:::

MAP_SHARED:  多个APP都调用mmap映射同一块内存时, 对内存的修改大家都可以看到。
             就是说多个APP、驱动程序实际上访问的都是同一块内存
MAP_PRIVATE: 创建一个copy on write的私有映射。
             当APP对该内存进行修改时,其他程序是看不到这些修改的。
             就是当APP写内存时, 内核会先创建一个拷贝给这个APP,
             这个拷贝是这个APP私有的, 其他APP、驱动无法访问。
1. 是文件IO的一种
2. 是用户空间和内核空间交互的一种高效的方式
3. 将内核空间的物理地址映射到用户空间的虚拟地址
------------用户空间--------------
//实现映射
//参数1:指定映射的起始,如果是NULL表示由系统自己来指定
//参数2:映射大小
//参数3:权限
//参数4:表示当前的映射区域是共享还是私有
//参数5:文件描述符
//参数6:偏移
addr = mmap(NULL, PAGE_SIZE, PROT_READ|PROT_WRITE, MAP_SHARED,fd, 0);
----------内核空间----------------
//虚拟地址转化为物理地址
addr = (unsigned long)__virt_to_phys(vm_addr);
//实现内存映射
//参数1:虚拟地址区域的描述对象
//参数2:虚拟地址的起始
//参数3:页帧号
//参数4:要映射的大小
//参数5:访问权限
ret = remap_pfn_range(vm_area, vm_area->vm_start, addr >> PAGE_SHIFT, PAGE_SIZE, vm_area->vm_page_prot);

五. 同步互斥

1. 概念
:::info
一句话理解同步与互斥:我等你用完厕所,我再用厕所。
什么叫同步?就是条件不允许,我要等等。
什么是互斥?你我早起都要用厕所,谁先抢到谁先用,中途不被打扰。
同步与互斥经常放在一起讲,是因为它们之的关系很大,“互斥”操作可以使用“同步”来实现。我“等”你用完厕所,我再用厕所

并发(concurrency)指的是多个执⾏单元同时、并⾏被执⾏,⽽并发的执⾏单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)

竞态发⽣的情况:
1.在单核CPU中,如果内核⽀持抢占,会产⽣竞态
2.在多核处理器中,核与核之间会产⽣竞态
3.中断和进程间会也会产⽣竞态
:::
2. 原子操作(理解:原子不可分割,整体运行)

函数名	                  作用
atomic_read(v)	        读出原子变量的值,即v->counter
atomic_set(v,i)	        设置原子变量的值,即v->counter = i
atomic_inc(v)	        v->counter++
atomic_dec(v)	        v->counter--
atomic_add(i,v)	        v->counter += i
atomic_sub(i,v)       	v->counter -= i
atomic_inc_and_test(v)	先加1,再判断新值是否等于0;等于0的话,返回值为1
atomic_dec_and_test(v)	先减1,再判断新值是否等于0;等于0的话,返回值为1

3. 互斥锁

函数名	                                      作用
mutex_init(mutex)	                        初始化一个struct mutex指针
DEFINE_MUTEX(mutexname)	                    初始化struct mutex mutexname
mutex_is_locked(struct mutex *lock)  	    判断mutex的状态 1:被锁了(locked) 0:没有被锁
mutex_lock(struct mutex *lock)	            获得mutex,如果暂时无法获得,休眠返回之时必定是已经获得了mutex
mutex_lock_interruptible(struct mutex *lock)获得mutex,如果暂时无法获得,休眠;休眠过程中可以被信号唤醒
mutex_lock_killable(struct mutex *lock)	    跟mutex_lock_interruptible类似,
mutex_lock_interruptible                    可以被任意信号唤醒,但mutex_lock_killable只能被“fatal signal”唤醒
mutex_trylock(struct mutex *lock)	        尝试获取mutex,如果无法获得,不会休眠,
mutex_unlock(struct mutex *lock)            释放mutex,会唤醒其他等待同一个mutex的线程
atomic_dec_and_mutex_lock(atomic_t *cnt, struct mutex *lock)让原子变量的值减1,如果减1后等于0,则获取mutex,

4. 信号量

函数名	                                      作用
DEFINE_SEMAPHORE(name)	                    定义一个struct semaphore name结构体,count值设置为1
sema_init(struct semaphore *sem, int val)	初始化semaphore
down(struct semaphore *sem)	                获得信号量,如果暂时无法获得就会休眠
down_interruptible(struct semaphore *sem)	获得信号量,如果暂时无法获得就会休眠,休眠过程有可能收到信号而被唤醒,
down_killable(struct semaphore *sem)	    down_killable只能被“fatal signal”唤醒,
down_trylock(struct semaphore *sem)	        尝试获得信号量,不会休眠,
down_timeout(struct semaphore *sem, long jiffies)	获得信号量,如果不成功,休眠一段时间
down_timeout                                休眠过程中,它不会被信号唤醒
up(struct semaphore *sem)	释放信号量,唤醒其他等待信号量的进程

semaphore和mutex的区别:

semaphore中可以指定count为任意值,比如有10个厕所,所以10个人都可以使用厕所。
而mutex的值只能设置为1或0,只有一个厕所。
是不是把semaphore的值设置为1后,它就跟mutex一样了呢?不是的。
一个mutex只能在进程上下文中使用:谁给mutex加锁,就只能由谁来解锁。
而semaphore并没有这些限制,它可以用来解决“读者-写者”问题:程序A在等待数据──想获得锁,程序B产生数据后释放锁,这会唤醒A来读取数据。semaphore的锁定与释放,并不限定为同一个进程

主要区别列表如下:
几把锁
谁能解锁
多次解锁
循环加锁
任务在持有锁的期间可否退出
硬件中断、软件中断上下文中使用
semaphore
任意,可设置
别的程序、中断等都可以
可以
可以
可以
可以
mutex
1
谁加锁,就得由谁解锁
不可以,因为只有1把锁
不可以,因为只有1把锁
不建议,容易导致死锁
不可以
5. 自旋锁

	函数名	                                作用
spin_lock_init(_lock)	              初始化自旋锁为unlock状态
void spin_lock(spinlock_t *lock)	  获取自旋锁(加锁),返回后肯定获得了锁
int spin_trylock(spinlock_t *lock)	  尝试获得自旋锁,成功获得锁则返回1,否则返回0
void spin_unlock(spinlock_t *lock)	  释放自旋锁,或称解锁
int spin_is_locked(spinlock_t *lock)  返回自旋锁的状态,已加锁返回1,否则返回0
自旋锁的加锁、解锁函数是:spin_lock、spin_unlock,还可以加上各种后缀,这表示在加锁或解锁的同时,还会做额外的事情:
后缀	               描述
_bh()	            加锁时禁止下半部(软中断),解锁时使能下半部(软中断)
_irq()	            加锁时禁止中断,解锁时使能中断
_irqsave/restore()	加锁时禁止并中断并记录状态,解锁时恢复中断为所记录的状态

6. 总结

1> 中断屏蔽:
中断屏蔽只针对单核cpu⽣效,因为单核处理的的并发产⽣都离不开中断的参与,所以屏蔽中断后,就可以解决竞态问题。 (不建议使用,会导致所有的中断屏蔽)
【要求】
Ø 中断屏蔽的时间要尽可能的短
Ø 如果中断屏蔽的时间⽐较⻓,可能会导致⽤户数据的丢失和内核的崩溃。
Ø 中断屏蔽保护的临界资源⽐较⼩

2> ⾃旋锁:
⾃旋锁⼜叫忙等待锁, ⾃旋锁期间不能有睡眠的函数存在,也不能主动放弃cpu的调度权,也不能进⾏耗时操作, 否则容易造成死锁。
【特点】
1.针对多核处理器设计的。
2.⾃旋状态是需要消耗cpu资源的。
3.⾃旋锁可能会导致死锁(在同⼀个进程内多次获取同⼀把未解锁的锁)。
4.⾃旋锁⼯作在中断上下⽂。
5.⾃旋锁保护的临界资源尽可能的⼩, ⾃旋锁上锁期间不能调⽤延时、schedule()(主动放弃cpu)、
copy_from_user/copy_to_user等,可能会导致内核崩溃。
6.⾃旋锁上锁的时候关闭抢占,CPU是禁⽌抢占的。

3> 信号量:
信号量是内核中⽤来保护临界资源的⼀种,与应⽤层信号量理解⼀致。 当⼀个进程占⽤信号量之后,另外⼀个进程也想获取信号量,此时后⼀个进程就处在休眠状态。
【特点】
1.在等待信号量的时候不消耗cpu资源。
2.信号量⼯作在进程上下⽂。
3.信号量保护的临界区可以很⼤,⾥⾯可以有延时,耗时,甚⾄休眠的操作。
4.信号量不会产⽣死锁

4> 互斥体:
当⼀个进程占⽤互斥体之后,另外⼀个进程也想获取互斥体,此时后⼀个进程就处在休眠状态。
【特点】
1.在等待互斥体的时候不消耗cpu资源
2.互斥体⼯作在进程上下⽂
3.互斥体保护的临界区可以很⼤,⾥⾯可以有延时,耗时,甚⾄休眠的操作。
4.互斥体不会产⽣死锁
5.对于临界区⽐较⼩的资源上锁,使⽤互斥体⽐信号量的效率⾼(互斥体在从 运⾏状态到休眠状态切换的时候,稍微等⼀会⼉再进⼊休眠状态)

5>原⼦操作:
对原⼦变量的操作,看成⼀个完整整体,对原⼦变量的操作不会被打断, 内部是通过内联汇编完成的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值