最近一直在学Linux字符设备驱动,从最基础的轮询读取,一步步到中断、底半部机制,再到input子系统和内核定时器。
一、IO读取方式:轮询与阻塞IO
用户态读取设备,大体分两种思路:非阻塞轮询 、阻塞IO 。
轮询就是在循环里不停调用read(),CPU反复查询设备状态,CPU占用高;阻塞IO是驱动里让进程休眠,等事件到来之后再唤醒进程,空闲时CPU可以调度别的任务,效率更高。
要在驱动实现阻塞等待,核心依靠等待队列,4个常用接口:
wait_event(wq, condition):不可中断等待。条件不满足就一直阻塞,收到信号也不会唤醒。wait_event_interruptible(wq, condition):可中断等待。事件发生或者收到用户态信号,都会唤醒进程,返回非0值代表被信号打断。wait_event_timeout(wq, condition, timeout):带超时的不可中断等待,超时自动唤醒。wait_event_interruptible_timeout(wq, condition, timeout):带超时的可中断等待。
唤醒对应的接口:
wake_up() 配合wait_event;wake_up_interruptible() 配合wait_event_interruptible。
注意:等待队列的条件变量,是共享资源,多并发场景下要考虑加锁保护。
二、中断基础:设备树获取中断号,注册中断
Linux 中断处理分为顶半部与底半部:顶半部在硬中断上下文快速完成硬件相关紧急操作,底半部延后处理耗时任务,以此缩短关中断时间,提升系统实时性。
Linux驱动中使用外部中断,标准流程:
- 设备树 里配置中断信息:
interrupt-parent指定中断控制器,interrupts写中断号和触发方式。 - 在
probe函数,用of_irq_get()从设备树节点拿到中断号。旧方式也可以用platform_get_resource获取。 - 使用
request_irq/devm_request_irq注册中断处理函数(顶半部)。devm版本的好处是资源会自动释放,不容易内存泄漏。 - 中断处理函数(顶半部)执行要快,不能做耗时操作,不能休眠。
中断相关接口:
request_irq:注册中断处理函数
free_irq:释放中断号
disable_irq:关中断
enable_irq:开中断
中断资源保护:自旋锁
中断上下文里不能用互斥锁(会休眠),中断里面加锁只能选自旋锁spinlock。
spin_lock / spin_unlock:基础自旋锁。单核下只是关闭抢占;多核下循环忙等直到拿到锁。
spin_lock_irqsave / spin_unlock_irqrestore:上锁同时关闭本地中断,防止中断抢占临界区。
关键点:
- 单核CPU:spinlock只关闭高优先级抢占;
- 多核CPU:多个CPU之间会循环争抢锁;
- 自旋锁持有期间代码不能休眠,一旦休眠直接死锁。
三、中断底半部:tasklet 和 workqueue
中断顶半部要求执行极快,硬件信号应答必须在这里完成;耗时操作要放到底半部。两种常用实现:
- tasklet :属于中断上下文。依然运行在中断环境,不允许休眠,调度后延迟执行。适合短任务。
- workqueue(工作队列) :属于进程上下文。允许msleep、阻塞,适合有延时、耗时的任务。
按键驱动Demo:阻塞IO + 等待队列 + workqueue底半部
这个驱动使用platform框架+misc杂项设备,按下按键触发中断,中断里唤醒等待队列,用户态read阻塞等待按键按下,同时使用工作队列做底半部任务。
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/of_irq.h>
#include <linux/workqueue.h>
static int keygpiono = 0;
static wait_queue_head_t wq;
static int condition = 0;
static int irqno = 0;
static struct work_struct workqueue;
static void key_workqueue_func(struct work_struct *work)
{
msleep(10);
pr_info("key pressed\n");
return;
}
static void key_func(unsigned long data)
{
msleep(10);
pr_info("key pressed\n");
return;
}
static DECLARE_TASKLET(mytasklet, key_func, 0);
static irqreturn_t key_handler(int irqno, void *pdev)
{
condition = 1;
wake_up_interruptible(&wq);
schedule_work(&workqueue);
//tasklet_schedule(&mytasklet);
return IRQ_HANDLED;
}
static ssize_t key_read(struct file *fp, char __user *puser, size_t n, loff_t *off)
{
unsigned long nret = 0;
int value = 0;
condition = 0;
wait_event_interruptible(wq, condition);
value = gpio_get_value(keygpiono);
nret = copy_to_user(puser, &value, sizeof(value));
if (nret) {
pr_info("copy_to_user failed\n");
return -1;
}
return sizeof(value);
}
static struct file_operations fops = {
.owner = THIS_MODULE,
.read = key_read,
};
static struct miscdevice key_misc = {
.minor = MISC_DYNAMIC_MINOR,
.name = "key_misc",
.fops = &fops,
};
static int key_probe(struct platform_device *pdevice)
{
int ret = 0;
ret = misc_register(&key_misc);
if (ret) {
pr_info("misc_register failed\n");
return -1;
}
INIT_WORK(&workqueue, key_workqueue_func);
init_waitqueue_head(&wq);
keygpiono = of_get_named_gpio(pdevice->dev.of_node, "gpio-key", 0);
if (keygpiono < 0) {
pr_info("of_get_named_gpio failed\n");
return -1;
}
ret = devm_gpio_request(&pdevice->dev, keygpiono, "key_platform_drv");
if (ret) {
pr_info("devm_gpio_request failed\n");
return -1;
}
irqno = of_irq_get(pdevice->dev.of_node, 0);
if (irqno < 0) {
pr_info("of_irq_get failed\n");
return -1;
}
ret = devm_request_irq(&pdevice->dev, irqno, key_handler, IRQF_TRIGGER_FALLING, "key_platform_drv", NULL);
if (ret) {
pr_info("request_irq failed\n");
return -1;
}
gpio_direction_input(keygpiono);
pr_info("key_probe success\n");
return 0;
}
static int key_remove(struct platform_device *pdevice)
{
misc_deregister(&key_misc);
pr_info("key_remove success\n");
return 0;
}
static struct platform_device_id key_id_tables[] = {
{ .name = "putekey"},
{},
};
static struct of_device_id key_of_match_table[] = {
{.compatible = "pute,putekey"},
{},
};
struct platform_driver key_drv = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "putekey",
.owner = THIS_MODULE,
.of_match_table = key_of_match_table,
},
.id_table = key_id_tables,
};
module_platform_driver(key_drv);
MODULE_LICENSE("GPL");
这个代码里msleep(10)放在workqueue没问题,如果放到tasklet或者中断顶半部就会直接报错,因为中断上下文不能休眠。
四、Input子系统:标准输入设备驱动
上面是自己写misc设备上报按键值。Linux内核提供了input子系统,专门统一管理按键、触摸屏、鼠标这类输入设备。
使用input子系统的开发流程:
input_allocate_device():分配input设备结构体input_set_capability():告诉内核这个设备具备什么事件能力(EV_KEY按键事件)input_register_device():注册input设备- 中断触发后,调用
input_event()上报事件,input_sync()同步事件,通知应用层。
应用层可以通过/dev/input/eventX读取按键事件,不用自己写字符设备read逻辑。
Input子系统按键驱动代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/platform_device.h>
#include <linux/miscdevice.h>
#include <linux/of_gpio.h>
#include <linux/gpio.h>
#include <linux/uaccess.h>
#include <linux/wait.h>
#include <linux/sched.h>
#include <linux/interrupt.h>
#include <linux/irqreturn.h>
#include <linux/of_irq.h>
#include <linux/workqueue.h>
#include <linux/input.h>
static int irqno = 0;
struct input_dev *pkey_dev = NULL;
static void mytasklet_func(unsigned long data)
{
input_event(pkey_dev, EV_KEY, KEY_0, 1);
input_sync(pkey_dev);
input_event(pkey_dev, EV_KEY, KEY_0, 0);
input_sync(pkey_dev);
return;
}
static DECLARE_TASKLET(mytasklet, mytasklet_func, 0);
static irqreturn_t key_isr(int irqno, void *pdev)
{
tasklet_schedule(&mytasklet);
return IRQ_HANDLED;
}
static int key_probe(struct platform_device *pdevice)
{
int ret = 0;
pkey_dev = input_allocate_device();
if (NULL == pkey_dev) {
pr_info("input_allocate_device failed\n");
return -1;
}
input_set_capability(pkey_dev, EV_KEY, KEY_0);
ret = input_register_device(pkey_dev);
if (ret) {
pr_info("input_register_device failed\n");
return -1;
}
irqno = of_irq_get(pdevice->dev.of_node, 0);
if (irqno < 0) {
pr_info("of_irq_get failed\n");
return -1;
}
ret = devm_request_irq(&pkey_dev->dev, irqno, key_isr, IRQF_TRIGGER_FALLING, "key_platform_input_drv", NULL);
if (ret) {
pr_info("devm_request_irq failed\n");
return -1;
}
return 0;
}
static int key_remove(struct platform_device *pdevice)
{
input_unregister_device(pkey_dev);
input_free_device(pkey_dev);
return 0;
}
static struct platform_device_id key_id_tables[] = {
{ .name = "putekey"},
{},
};
static struct of_device_id key_of_match_table[] = {
{.compatible = "pute,putekey"},
{},
};
struct platform_driver key_drv = {
.probe = key_probe,
.remove = key_remove,
.driver = {
.name = "putekey",
.owner = THIS_MODULE,
.of_match_table = key_of_match_table,
},
.id_table = key_id_tables,
};
module_platform_driver(key_drv);
MODULE_LICENSE("GPL");
五、内核定时器
内核定时器依靠系统节拍jiffies,HZ代表每秒节拍数,HZ=100就是1秒100次节拍。
定时器属于一次性触发,到点后执行回调函数。回调运行在中断上下文,不能休眠。
开发步骤:
- 定义timer结构体,设置超时回调函数
init_timer初始化add_timer添加定时器mod_timer修改定时超时时间del_timer删除定时器
内核定时器示例代码
#include <linux/fs.h>
#include <linux/module.h>
#include <linux/cdev.h>
#include <linux/init.h>
#include <linux/timer.h>
static void timeout(unsigned long data)
{
pr_info("timeout ok!\n");
return;
}
static DEFINE_TIMER(mtimer, timeout, 0, 0);
static int __init kernel_timer_init(void)
{
init_timer(&mtimer);
add_timer(&mtimer);
mod_timer(&mtimer, jiffies + msecs_to_jiffies(100));
pr_info("add timer success\n");
return 0;
}
static void __exit kernel_timer_exit(void)
{
del_timer(&mtimer);
pr_info("del timer success\n");
return;
}
module_init(kernel_timer_init);
module_exit(kernel_timer_exit);
MODULE_LICENSE("GPL");
六、延时函数
udelay():忙等待延时,CPU空转,延时精度高,适合微秒级,不能休眠。msleep():休眠延时,让出CPU调度,精度一般,进程上下文可用,中断上下文不能调用。msleep_interruptible():可被信号打断的休眠延时。
这段时间把中断上下半部、等待队列、input子系统、内核定时器串联起来,可以明显感受到Linux驱动的设计思想:顶半部快处理,底半部做耗时任务;阻塞IO减少CPU占用;子系统封装通用设备逻辑 。
之前写的misc按键驱动是自己从零实现字符设备,而input子系统是内核提供好的框架,直接上报标准事件,应用层不用适配特殊设备文件,这也是内核子系统的优势。