Linux 驱动基础
注册驱动模块时给模块传递参数
在一些情况下,我们要动态的改变驱动中某个变量的值,那么就可以在注册时给驱动模块传递参数。
给驱动模块中传递参数,需要定义好接受参数值的全局变量,并调用module_param
来引用它,具体示例如下:
include\linux\ Moduleparam.h
module_param(name,type,perm)
参数:@name用来接收参数的变量名@type参数的数据类型bool :布尔类型invbool:颠倒了值的bool类型;charp :字符指针类型,内存为用户提供的字符串分配;int :整型long :长整型short :短整型uint :无符号整型ulong :无符号长整型ushort :无符号短整型@perm 指定参数访问权限。
传递一个int 型参数
//驱动代码
static unsigned int var=0;
module_param(var,uint,0400);
在加载模块的时候,传递参数:
insmod test.ko var=100
传递string 参数
static char *string;
module_param(string,charp,0400);
在加载模块的时候,传递参数:
insmod test.ko string="abcdefg";
驱动符号导出
(1)什么是符号?
这里的符号主要指的是全局变量和函数。
(2)为什么要导出符号?
Linux内核采用的是以模块化形式管理内核代码。内核中的每个模块相互之间是相互独立的,也就是说A模块的全局变量和函数,B模块是无法访问的。
如果一个模块已经以静态的方式编译进的内核,那么它导出的符号就会出现在全局的内核符号表中(内核源码根目录下的Module.symvers 文件)。
当我们想在A 模块中引用动态插入(obj-m = xxx.o) 的B 模块定义的全局变量或者函数时就需要导出。
如下图,内核中已经定义了函数 irq_set_irqchip_state,而且我的代码中已经包含了它的头文件,但还是报错未定义,这就是因为这个符号未导出,在动态加载的.ko 中不认识它。
解决方法:EXPORT_SYMBOL(irq_set_irqchip_state);
(3)如何导出符号?
Linux内核给我们提供了两个宏:
EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);
(4)模块编译时,如何寻找使用的符号?
a.在本模块中符号表中,寻找符号(函数或变量实现)
b.在内核全局符号表中寻找
c.在模块目录下的Module.symvers文件中寻找
调试流程:在B模块中声明全局变量和函数,然后用EXPORT_SYMBOL 导出,编译完成后会在当前模块目录下生成Module.symvers,将其拷贝到A 模块目录,然后在A 模块中调用extern 修饰导出的变量和函数;
模块B 代码
static int global_var = 100;
static void show(void)
{printk("show(): global_var =%d \n",global_var);
}
EXPORT_SYMBOL(global_var);
EXPORT_SYMBOL(show);
模块A 代码
extern int global_var;
extern void show(void);
驱动中访问寄存器
在Linux中,无论是应用程序或是驱动程序,访问的都是虚拟地址,所以在访问一个物理寄存器地址时,首先要进行映射。
将一个寄存器物理地址映射为虚拟地址可以使用ioremap
函数,反映射 ioumap
。
//包含头文件#include <asm/io.h> 对于arm其实包含的就是arch/arm/include/asm/io.h
#define ioremap(cookie,size) __arm_ioremap((cookie), (size),MT_DEVICE)
void __iomem * __arm_ioremap(phys_addr_t phys_addr, size_t size,
unsigned int mtype)
{return arch_ioremap_caller(phys_addr, size, mtype,__builtin_return_address(0));
}
/*
cookie: 要映射的物理地址
size: 要映射的地址长度
返回值:映射好的虚拟地址 (void __iomem * 类型)
*/
void iounmap (volatile void __iomem *addr)
映射完成后可以用以下函数访问寄存器的值:
//小端读写:如果要读的寄存器和cpu 都是小端,那么可以用以下的小端读写函数
u8 readb(const volatile void __iomem *addr)
u16 readw(const volatile void __iomem *addr)
u32 readl(const volatile void __iomem *addr)void writeb(u8 value, volatile void __iomem *addr)
void writew(u16 value, volatile void __iomem *addr)
void writel(u32 value, volatile void __iomem *addr)//大端读写:如果cpu 是小端,寄存器是大端,可以用以下大端函数读写
#define ioread16be(p) ({ __u16 __v = be16_to_cpu((__force __be16)__raw_readw(p)); __iormb(); __v; })
#define ioread32be(p) ({ __u32 __v = be32_to_cpu((__force __be32)__raw_readl(p)); __iormb(); __v; })#define iowrite16be(v,p) ({ __iowmb(); __raw_writew((__force __u16)cpu_to_be16(v), p); })
#define iowrite32be(v,p) ({ __iowmb(); __raw_writel((__force __u32)cpu_to_be32(v), p); })
注释:iomem 映射与普通内存映射的区别,iomem 主要用于寄存器,寄存器每次值的改变都有独特的意义,比如写0,写1 可能是让某个gpio输出高低。而普通内存一般只用来存放数据。
定义指向寄存器虚拟地址的指针时可以用volatile 来修饰
volatile的作用:volatile
1.防止编译器的优化
2.每次直接从内存中读取
代码示例:
/* 寄存器物理地址 */
#define CCM_CCGR1_BASE (0X020C406C)
#define SW_MUX_GPIO1_IO03_BASE (0X020E0068)
#define SW_PAD_GPIO1_IO03_BASE (0X020E02F4)
#define GPIO1_DR_BASE (0X0209C000)
#define GPIO1_GDIR_BASE (0X0209C004/* 映射后的寄存器虚拟地址指针 */
static void __iomem *IMX6U_CCM_CCGR1; //void iomem* 类型中可能已经包含了volatile 修饰
static void __iomem *SW_MUX_GPIO1_IO03;
static void __iomem *SW_PAD_GPIO1_IO03;
static void __iomem *GPIO1_DR;
static void __iomem *GPIO1_GDIR;/* 寄存器地址映射 */
IMX6U_CCM_CCGR1 = ioremap(CCM_CCGR1_BASE, 4);
SW_MUX_GPIO1_IO03 = ioremap(SW_MUX_GPIO1_IO03_BASE, 4);
SW_PAD_GPIO1_IO03 = ioremap(SW_PAD_GPIO1_IO03_BASE, 4);
GPIO1_DR = ioremap(GPIO1_DR_BASE, 4);
GPIO1_GDIR = ioremap(GPIO1_GDIR_BASE, 4);/* 读写寄存器地址*/
val = readl(IMX6U_CCM_CCGR1);
writel(val, IMX6U_CCM_CCGR1);/* 取消映射 */
iounmap(IMX6U_CCM_CCGR1); //在自己编写的驱动中反映射时经常出现异常(卡住或者oops),待研究
iounmap(SW_MUX_GPIO1_IO03);
iounmap(SW_PAD_GPIO1_IO03);
iounmap(GPIO1_DR);
iounmap(GPIO1_GDIR);
休眠与唤醒
当应用程序必须等待某个事件发生,比如必须等待按键被按下时, 可以使用“休眠-唤醒”机制。 效果类似于阻塞IO。
等待队列
使用宏DECLARE_WAIT_QUEUE_HEAD 定义一个等待队列:
相关函数定义于:include/linux/wait.h
static DECLARE_WAIT_QUEUE_HEAD(gpio_key_wq);
#define DECLARE_WAIT_QUEUE_HEAD(name) \wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
休眠函数
用下列函数可以让一个线程挂起:
唤醒函数
使用下列函数可以唤醒线程:
POLL 机制
使用休眠-唤醒的方式等待某个事件发生时,有一个缺点: 等待的时间可能很久。我们可以加上一个超时时间,这时就可以使用 poll 机制。
poll机制: 应用层调用poll 函数查询是否有数据可读 或 有空间可写。如果可读写,直接返回可读写状态;没有数据可读或不可写,那么进入休眠状态,直到可读写或超时后返回。
驱动编程
使用 poll 机制时,驱动程序的核心就是提供对应的 drv_poll (file_operations->poll)函数。应用层调用 poll -> sys_poll -> drv_poll;
在 drv_poll 函数中要做 2 件事:
① 把当前线程挂入队列 wq: poll_wait
我们需要在drv_poll 函数中调用poll_wait(),把线程挂入等待队列。
应用程序调用一次 poll,可能导致 drv_poll 被调用 2 次,但是我们并不需要把当前线程挂入队列 2 次。
可以使用内核的函数 poll_wait 把线程挂入队列,如果线程已经在队列里了,它就不会再次挂入。
调用poll_wait 并不会直接挂起线程,只是把线程放入等待队列。
② 返回设备状态:
APP 调用 poll 函数时,有可能是查询“有没有数据可以读”: POLLIN,也有可能是查询“你有没有空间给我写数据”: POLLOUT。
所以 drv_poll 要返回自己的当前状态: (POLLIN | POLLRDNORM) 或 (POLLOUT | POLLWRNORM)。
POLLRDNORM 等同于 POLLIN,为了兼容某些 APP 把它们一起返回。
POLLWRNORM 等同于 POLLOUT ,为了兼容某些 APP 把它们一起返回。
//include/linux/poll.h
static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
wait_address:等待队列。
驱动中唤醒线程依旧使用 wake_up_interruptible 等函数。
应用编程
可以使用 man poll
命令查看poll 函数使用方法。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);struct pollfd {int fd; /* file descriptor */short events; /* requested events */short revents; /* returned events */};
fds:
fd:open 返回的句柄。
envents:需要查询的事件,读时 envents = POLLIN,写时 envents = POLLOUT。
nfds:传入poll 的fds 数量。
timeout:超时时间,ms。
返回值:0,不可读写;其余参考下表。
异步通知
使用休眠唤醒或poll 时都有一个缺点,应用程序需要陷入休眠状态等待数据的到来;如果app 在没有数据的期间还想做其它事情怎么办?可以使用异步通知。
什么是异步通知?
你在旁边等着, 眼睛盯着店员, 生怕别人插队, 他一做好你就知道: 你是主动等待他做好, 这叫“ 同步”。
你付钱后就去玩手机了, 店员做好后他会打电话告诉你: 你是被动获得结果, 这叫“ 异步”。
当前要实现的场景是:
app 在处理其他事,比如不停的打印:printf(“1111111111\n”);
按键按下时,驱动中的中断获取到键值,并使用异步通知告诉app 有数据来了;app 调用read 读取键值。
那么就需要思考以下问题:
APP 要做什么:接收信号。
① 发什么信号
Linux提供很多信号,SIGIO 是Linux驱动比较常用的信号,表示有IO数据。
② 内核里有那么多驱动, 你想让哪一个驱动给你发信号?
APP 要打开驱动程序的设备节点。 //open() 打开/dev/xxx
③ 驱动程序怎么知道要发信号给你而不是别人?
APP 要把自己的进程 ID 告诉驱动程序。 //fcntl(fd,F_SETOWN,getpid());
④ 使能驱动可以使用异步通知。
获取驱动文件 flag //oflags = fcntl(fd,F_GETFL);
使能驱动异步通知 //fcntl(fd,F_SETFL,oflags | FASYNC);
④ APP 有时候想收到信号, 有时候又不想收到信号:
应该可以把 APP 的意愿告诉驱动。
⑤ Linux中有许多信号,app 需要接收哪个信号,收到信号后需要做什么事?
注册信号处理函数,绑定信号与信号处理函数。使用signal 函数注册。
驱动需要做哪些事情:发信号
① 记录app 进程ID。
app 设置pid 时,首先会被保存到file 结构体;
② APP 还要使能驱动程序的异步通知功能, 驱动中有对应的函数:
判断flag 的FASYNC 变化后,会调用drv_fasync,在drv_fasync() 中调用fasync_helper(),它会根据 FAYSNC 的值决定是否设置 button_async->fa_file=驱动文件 file;
③ 发生中断时, 有数据时, 驱动程序调用内核辅助函数发信号。
这个辅助函数名为 kill_fasync。
file 结构体: 应用程序打开设备节点时,会在内核VFS 层建立一个struct file 来描述一个文件的动态信息。
关于更多的file 结构体内容可以通过以下博文了解
字符设备的应用程序到驱动的调用流程
手把手教Linux驱动4-进程、文件描述符、file、inode关系详解
struct file {union {struct llist_node fu_llist;struct rcu_head fu_rcuhead;} f_u;struct path f_path;struct inode *f_inode; /* cached value */const struct file_operations *f_op;/** Protects f_ep_links, f_flags.* Must not be taken from IRQ context.*/spinlock_t f_lock;atomic_long_t f_count;unsigned int f_flags;fmode_t f_mode;struct mutex f_pos_lock;loff_t f_pos;struct fown_struct f_owner;const struct cred *f_cred;struct file_ra_state f_ra;u64 f_version;
#ifdef CONFIG_SECURITYvoid *f_security;
#endif/* needed for tty driver, and maybe others */void *private_data;#ifdef CONFIG_EPOLL/* Used by fs/eventpoll.c to link all the hooks to this file */struct list_head f_ep_links;struct list_head f_tfile_llink;
#endif /* #ifdef CONFIG_EPOLL */struct address_space *f_mapping;
}
程序流程:
app 需要调用的函数:
signal、fcntl
用法使用:man signal
查询
驱动编程
使用异步通知时,驱动程序的核心有 2:
① 提供对应的 drv_fasync 函数;
② 并在合适的时机发信号。
drv_fasync 函数很简单,调用 fasync_helper 函数就可以,如下:
static struct fasync_struct *button_async;
static int drv_fasync (int fd, struct file *filp, int on)
{return fasync_helper (fd, filp, on, &button_async);
}
调用 faync_helper,它会根据 FAYSNC 的值决定是否设置 button_async->fa_file=驱动文件 filp:
驱动文件 filp 结构体里面含有之前设置的 PID。
fasync_helper 函数会分配、构造一个 fasync_struct 结构体 button_async:
① 驱动文件的 flag 被设置为 FAYNC 时:
button_async->fa_file = filp; // filp 表示驱动程序文件,里面含有之前设置的 PID
② 驱动文件被设置为非 FASYNC 时:
button_async->fa_file = NULL;
以后想发送信号时,使用 button_async 作为参数就可以,它里面“可能”含有 PID。
什么时候发信号呢?在本例中,在 GPIO 中断服务程序中发信号。
怎么发信号呢?代码如下:
kill_fasync (&button_async, SIGIO, POLL_IN);
第 1 个参数: button_async->fa_file 非空时,可以从中得到 PID,表示发给哪一个 APP;
第 2 个参数表示发什么信号: SIGIO;
第 3 个参数表示为什么发信号: POLL_IN,有数据可以读了。 (APP 用不到这个参数)
内核定时器
在配置内核 make menuconfig 时,可以搜索选项 CONFIG_HZ 来查看内核的心跳。
默认 CONFIG_HZ=100,这意味着内核1s 内会发生一百次系统中断,也就是每隔10ms 产生一次系统中断。
10ms 一次心跳,暂时称之为一个滴答。
内核定时器就是依据Linux 系统中断实现的,在CONFIG_HZ=100 的情况下内核定时器的最小精度就是10ms。
在内核中有一个全局变量 jiffies 来统计系统启动至今总的滴答数。
另外内核中有个全局的宏 HZ 它的值等于CONFIG_HZ 设置的数。
在内核中使用定时器很简单,涉及这些函数(参考内核源码 include\linux\timer.h):
Linux 内核使用一个struct timer_list 来描述一个定时器:
struct timer_list {struct list_head entry;unsigned long expires; /* 定时器超时时间,单位是节拍数 */struct tvec_base *base;void (*function)(unsigned long); /* 定时处理函数 */unsigned long data; /* 要传递给 function 函数的参数 */int slack;
};
① setup_timer(timer, fn, data):
设置定时器,主要是初始化 timer_list 结构体,设置其中的函数、参数。
② void add_timer(struct timer_list *timer):
向内核添加定时器。 timer->expires 表示超时时间。
当超时时间到达,内核就会调用这个函数: timer->function(timer->data)。
③ int mod_timer(struct timer_list *timer, unsigned long expires):
修改定时器的超时时间,
它等同于: del_timer(timer); timer->expires = expires; add_timer(timer);
但是更加高效。
④ int del_timer(struct timer_list *timer):
删除定时器。
① 在 add_timer 之前,直接修改:
timer.expires = jiffies + xxx; // xxx 表示多少个滴答后超时,也就是 xxx*10ms
timer.expires = jiffies + 2*HZ; // HZ 等于 CONFIG_HZ, 2*HZ 就相当于 2 秒
② 在 add_timer 之后,使用 mod_timer 修改:
mod_timer(&timer, jiffies + xxx); // xxx 表示多少个滴答后超时,也就是 xxx*10ms
mod_timer(&timer, jiffies + 2*HZ); // HZ 等于 CONFIG_HZ, 2*HZ 就相当于 2 秒
利用内核定时器来处理按键抖动
在实际的按键操作中,可能会有机械抖动:
按下或松开一个按键,它的 GPIO 电平会反复变化,最后才稳定。一般是几十毫秒才会稳定。
如果不处理抖动的话,用户只操作一次按键,中断程序可能会上报多个数据。
怎么处理?
① 在按键中断程序中,可以循环判断几十亳秒,发现电平稳定之后再上报
② 使用定时器
显然第 1 种方法太耗时,违背“中断要尽快处理”的原则,你的系统会很卡。
怎么使用定时器?看下图:
核心在于:在 GPIO 中断中并不立刻记录按键值,而是修改定时器超时时间, 10ms 后再处理。
如果 10ms 内又发生了 GPIO 中断,那就认为是抖动,这时再次修改超时时间为 10ms。
只有 10ms 之内再无 GPIO 中断发生,那么定时器的函数才会被调用。
在定时器函数中记录按键值。
中断下半部 tasklet
在前面我们介绍过中断上半部、下半部。中断的处理有几个原则:
① 不能嵌套;
② 越快越好。
在处理当前中断时,即使发生了其他中断,其他中断也不会得到处理,所以中断的处理要越快越好。但
是某些中断要做的事情稍微耗时,这时可以把中断拆分为上半部、下半部。
在上半部处理紧急的事情,在上半部的处理过程中,中断是被禁止的;
在下半部处理耗时的事情,在下半部的处理过程中,中断是使能的。
内核函数
定义 tasklet
中断下半部使用结构体 tasklet_struct 来表示,它在内核源码 include\linux\interrupt.h 中定义:
struct tasklet_struct
{struct tasklet_struct *next;unsigned long state;atomic_t count;void (*func)(unsigned long);unsigned long data;
};
其中的 state 有 2 位:
① bit0 表示 TASKLET_STATE_SCHED
等于 1 时表示已经执行了 tasklet_schedule 把该 tasklet 放入队列了; tasklet_schedule 会判断该位,如果已经等于 1 那么它就不会再次把 tasklet 放入队列。
② bit1 表示 TASKLET_STATE_RUN
等于 1 时,表示正在运行 tasklet 中的 func 函数;函数执行完后内核会把该位清 0。
其中的 count 表示该 tasklet 是否使能:等于 0 表示使能了,非 0 表示被禁止了。对于 count 非 0 的tasklet,里面的 func 函数不会被执行。
使用中断下半部之前,要先实现一个 tasklet_struct 结构体,这可以用这 2 个宏来定义结构体:
#define DECLARE_TASKLET(name, func, data) \struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(0), func, data }
#define DECLARE_TASKLET_DISABLED(name, func, data) \struct tasklet_struct name = { NULL, 0, ATOMIC_INIT(1), func, data }
使用 DECLARE_TASKLET 定义的 tasklet 结构体,它是使能的;
使 用 DECLARE_TASKLET_DISABLED 定 义 的 tasklet 结 构 体 , 它 是 禁 止 的 ; 使 用 之 前 要 先 调 用tasklet_enable 使能它。
也可以使用函数来初始化 tasklet 结构体:
extern void tasklet_init(struct tasklet_struct *t,void (*func)(unsigned long), unsigned long data);
使能/禁止 tasklet
static inline void tasklet_enable(struct tasklet_struct *t);
static inline void tasklet_disable(struct tasklet_struct *t);
tasklet_enable 把 count 增加 1; tasklet_disable 把 count 减 1。
调度 tasklet
static inline void tasklet_schedule(struct tasklet_struct *t);
把 tasklet 放入链表,并且设置它的 TASKLET_STATE_SCHED 状态为 1。它需要放在中断上半部处理函数中调用。
** kill tasklet**
extern void tasklet_kill(struct tasklet_struct *t);
如果一个 tasklet 未被调度, tasklet_kill 会把它的 TASKLET_STATE_SCHED 状态清 0;
如果一个 tasklet 已被调度, tasklet_kill 会等待它执行完华,再把它的 TASKLET_STATE_SCHED 状态清 0。
通常在卸载驱动程序时调用 tasklet_kill。
tasklet 使用方法
先定义 tasklet,需要使用时(硬件中断处理函数)调用 tasklet_schedule,驱动卸载前调用 tasklet_kill。
tasklet_schedule 只是把 tasklet 放入内核队列,它的 func 函数会在软件中断的执行过程中被调用。
如下图,tasklet 在硬件中断处理函数执行完成后才执行,并不会直接在tasklet_schedule 函数中执行。
工作队列
前面讲的定时器、 下半部 tasklet, 它们都是在中断上下文中执行, 它们无法休眠。 当要处理更复杂的事情时, 往往更耗时。 这些更耗时的工作放在定时器或是tasklet中, 会使得系统很卡; 并且循环等待某件事情完成也太浪费 CPU 资源了。
如果使用线程来处理这些耗时的工作, 那就可以解决系统卡顿的问题: 因为线程可以休眠。
在内核中, 我们并不需要自己去创建线程, 可以使用“ 工作队列” (workqueue)。 内核初始化工作队列时, 就为它创建了内核线程。 以后我们要使用“ 工作队列”, 只需要把“ 工作” 放入“ 工作队列中”, 对应的内核线程就会取出“ 工作”, 执行里面的函数。
如下,可以使用ps 命令来查看系统上创建的工作线程。
root@OpenWrt:~# ps |grep "kworker"7 root 0 IW [kworker/u8:0-ev]32 root 0 IW [kworker/2:1-mm_]33 root 0 IW [kworker/3:1-eve]85 root 0 IW< [kworker/u9:0-hc]119 root 0 IW [kworker/0:1-mm_]120 root 0 IW [kworker/1:1-mm_]130 root 0 IW [kworker/u8:2-ev]147 root 0 IW [kworker/3:2-mm_]148 root 0 IW [kworker/0:2-eve]
工作队列的应用场合: 要做的事情比较耗时, 甚至可能需要休眠, 那么可以使用工作队列。
缺点: 多个工作(函数)是在某个内核线程中依序执行的, 前面函数执行很慢, 就会影响到后面的函数。在多 CPU 的系统下, 一个工作队列可以有多个内核线程, 可以在一定程度上缓解这个问题。
Linux 内核用work_struct 来描述一个工作,用workqueue_struct 描述一个工作队列,kworker 是工作线程。
工作队列:相当于一个缓冲区、一条流水线,用来存放待处理的工作。
kworker 线程:kworker 线程就是处理工作的人,有工作是它会被唤醒,没有工作时它会陷入休眠。
内核函数
内核线程、 工作队列(workqueue)都由内核创建了, 我们只是使用。 使用的核心是一个 struct work_struct
结构体, 定义如下:
使用工作队列时, 步骤如下:
① 构造一个 work_struct 结构体, 里面有函数;
② 把这个 work_struct 结构体放入工作队列, 内核线程就会运行 work 中的函数。
工作队列相关的结构体、函数等定义在 include\linux\workqueue.h,在编写驱动时可以参考头文件来加深对内核代码的印象。
定义 work
#define DECLARE_WORK(n, f) \struct work_struct n = __WORK_INITIALIZER(n, f)#define DECLARE_DELAYED_WORK(n, f) \struct delayed_work n = __DELAYED_WORK_INITIALIZER(n, f, 0)
第 1 个宏是用来定义一个 work_struct 结构体, 要指定它的函数。
第 2 个宏用来定义一个 delayed_work 结构体, 也要指定它的函数。 所以“ delayed”, 意思就是说要让它运行时, 可以指定: 某段时间之后你再执行。
如果要在代码中初始化 work_struct 结构体, 可以使用下面的宏:#define INIT_WORK(_work, _func)
他们的区别在于DECLARE_WORK 帮我们定义好了struct work_struct 并绑定func,而INIT_WORK 需要我们自己定义struct work_struct。
#define INIT_WORK(w, f) \do { \(w)->data = 0; \(w)->func = (f); \} while (0)
使用 work: schedule_work
调用 schedule_work 时, 就会把 work_struct 结构体放入队列中, 并唤醒对应的内核工作线程。 工作线程从队列里把 work_struct 结构体取出来, 执行里面的函数。
inline bool schedule_work(struct work_struct *work)
延时工作队列使用:mod_delayed_work
延时工作队列与定时器有一些类似,调用mod_delayed_work 可以在一段事件后执行work->func 函数中的内容。
内核的按键驱动(gpio-keys)就利用这个原理来消除按键抖动,在中断函数中调用mod_delayed_work
。 第一次触发中断激活延时工作,如果在10ms 内再次发生中断则重新修改延时时间,依次类推直到最后一次中断10ms 后执行延时工作任务。
phy 状态机利用这个机制在work->func 中调用mod_delayed_work 来不停的循环工作任务,读取phy 的寄存器状态。
static inline bool mod_delayed_work(struct workqueue_struct *wq, //workqueue_struct 可以指定系统工作队列systemd_wqstruct delayed_work *dwork,unsigned long delay)
内存管理
#include <stdio.h>
#include <stdlib.h>int a;void main(int argc,char **argv)
{a = atoi(argv[1]);printf("ptr %x ,value %d\n",&a,a);sleep(20);
}
先用一个简单的应用程序来测试一个现象:
先后执行两次程序,打印变量a的地址和a的值,执行第二个程序时第一个还在睡眠中。
yz@yanzhi-13:~/zjh/test_source$ ./hello 12 &
[1] 15933
yz@yanzhi-13:~/zjh/test_source$ ptr 601054 ,value 12
yz@yanzhi-13:~/zjh/test_source$
yz@yanzhi-13:~/zjh/test_source$ ./hello 123 &
[2] 15950
yz@yanzhi-13:~/zjh/test_source$ ptr 601054 ,value 123
yz@yanzhi-13:~/zjh/test_source$
yz@yanzhi-13:~/zjh/test_source$ ps |grep hello
15933 pts/14 00:00:00 hello
15950 pts/14 00:00:00 hello
应用进程的地址空间结构
发现一个问题两个进程中a 的地址是一样的,但是它们的值却不同,按道理来说为了保存不同的值 变量的地址必然是不一样的,这是怎么回事?
这里要引入虚拟地址的概念: CPU 发出的地址是虚拟地址, 它经过 MMU(Memory Manage Unit, 内存管理单元)映射到物理地址上, 对于不同进程的同一个虚拟地址, MMU 会把它们映射到不同的物理地址。
如下图:
每一个 APP 在内核里都有一个 task_struct, 这个结构体中保存有内存信息: mm_struct 。 而虚拟地址、物理地址的映射关系保存在页目录表中, 如下图所示:
解析如下:
① 每个 APP 在内核中都有一个 task_struct 结构体, 它用来描述一个进程;
② 每个 APP 都要占据内存, 在 task_struct 中用 mm_struct 来管理进程占用的内存;
内存有虚拟地址、 物理地址, mm_struct 中用 mmap 来描述虚拟地址, 用 pgd 来描述虚拟地址与物理地址之间的映射关系。
注意: pgd, Page Global Directory, 页目录。
③ 每个 APP 都有一系列的 VMA: virtual memory
比如 APP 含有代码段、 数据段、 BSS 段、 栈等等, 还有共享库。 这些单元会保存在内存里, 它们的地址空间不同, 权限不同(代码段是只读的可运行的、 数据段可读可写), 内核用一系列的 vm_area_struct 来描述它们。
vm_area_struct 中的 vm_start、 vm_end 是虚拟地址。
④ vm_area_struct 中虚拟地址如何映射到物理地址去?
每一个 APP 的虚拟地址可能相同, 物理地址不相同, 这些对应关系保存在 pgd 中。
ARM 架构内存映射简介
ARM 架构支持一级页表映射, 也就是说 MMU 根据 CPU 发来的虚拟地址可以找到第 1 个页表, 从第 1 个页表里就可以知道这个虚拟地址对应的物理地址。 一级页表里地址映射的最小单位是 1M。
ARM 架构还支持二级页表映射, 也就是说 MMU 根据 CPU 发来的虚拟地址先找到第 1 个页表, 从第 1 个页表里就可以知道第 2 级页表在哪里; 再取出第 2 级页表, 从第 2 个页表里才能确定这个虚拟地址对应的物理地址。 二级页表地址映射的最小单位有 4K、 1K, Linux 使用 4K。
一级页表项里的内容, 决定了它是指向一块物理内存, 还是指问二级页表, 如下图:
一级页表映射过程
一线页表中每一个表项用来设置 1M 的空间, 对于 32 位的系统, 虚拟地址空间有 4G, 4G/1M=4096。 所以一级页表要映射整个 4G 空间的话, 需要 4096 个页表项。
第 0 个页表项用来表示虚拟地址第 0 个 1M(虚拟地址为 0~0xFFFFF)对应哪一块物理内存, 并且有一些权限设置;
第 1 个页表项用来表示虚拟地址第 1 个 1M(虚拟地址为 0x100000~0x1FFFFF)对应哪一块物理内存, 并且有一些权限设置;
依次类推。
使用一级页表时, 先在内存里设置好各个页表项, 然后把页表基地址告诉 MMU, 就可以启动 MMU 了。
以下图为例介绍地址映射过程:
① CPU 发出虚拟地址 vaddr, 假设为 0x12345678
② MMU 根据 vaddr[31:20]找到一级页表项:
虚拟地址 0x12345678 是虚拟地址空间里第 0x123 个 1M, 所以找到页表里第 0x123 项, 根据此项内容知道它是一个段页表项。
段内偏移是 0x45678。
③ 从这个表项里取出物理基地址: Section Base Address, 假设是 0x81000000
④ 物理基地址加上段内偏移得到: 0x81045678
所以 CPU 要访问虚拟地址 0x12345678 时, 实际上访问的是 0x81045678 的物理地址。
二级页表映射过程
首先设置好一级页表、 二级页表, 并且把一级页表的首地址告诉 MMU。
以下图为例介绍地址映射过程:
① CPU 发出虚拟地址 vaddr, 假设为 0x12345678
② MMU 根据 vaddr[31:20]找到一级页表项:
虚拟地址 0x12345678 是虚拟地址空间里第 0x123 个 1M, 所以找到页表里第 0x123 项。 根据此项内容知道它是一个二级页表项。
③ 从这个表项里取出地址, 假设是 address, 这表示的是二级页表项的物理地址;
④ vaddr[19:12]表示的是二级页表项中的索引 index 即 0x45, 在二级页表项中找到第 0x45 项;
⑤ 二级页表项格式如下:
里面含有这 4K 或 1K 物理空间的基地址 page base addr, 假设是 0x81889000:
它跟 vaddr[11:0]组合得到物理地址: 0x81889000 + 0x678 = 0x81889678。
所以 CPU 要访问虚拟地址 0x12345678 时, 实际上访问的是 0x81889678 的物理地址。
多次设备的驱动编写
在Linux 系统上我们经常看到有一些设备它有许多次设备,比如串口,它们的设备名是相同的 设备名+序号,并且拥有相同的主设备号,递增的次设备号。
那么这些在驱动中是如何设置的:
设备名很简单,就是个名字,这些串口都是属于同一个驱动的,我们也可以在驱动中自定义一个设备的名字。
序号怎么得到呢?
在设备树中有这样别名的节点,把某个串口的名字定义成 serialx,网口的名字定义成ethernetx,然后再驱动中我们可以调用如下的函数来获取到别名后面的序号:
ret 就是获取到的序号;下方代码来自imx6ull 的串口驱动imx.c
设备号怎么定义:主设备号都是一样的,像普通驱动一样定义一个主设备号;然后指定一个基础的次设备号,加上获取到的序号就可以得到每个次设备唯一递增的次设备号。
然后我们需要创建一个类和为每一个串口创建一个设备节点,设备节点与struct device 有关,需要为每个串口创建一个device 并与设备号绑定,将前面的名字与序号组合起来赋值给device->name,最后注册device。
最后为了能通过文件IO 访问设备,要为每个设备创建cdev 并注册。
相关文章:

Linux 驱动基础
注册驱动模块时给模块传递参数 在一些情况下,我们要动态的改变驱动中某个变量的值,那么就可以在注册时给驱动模块传递参数。 给驱动模块中传递参数,需要定义好接受参数值的全局变量,并调用module_param 来引用它,具体…...

linux 共享内存操作(shm_open、mmap、编译链接库:-lz -lrt -lm -lc都是什么库)
文章目录linux 共享内存操作(shm_open)一、背景二、函数使用说明shm_openftruncate(改变文件大小)mmap共享内存三、示例代码创建内存共享文件并写入数据打开内存共享文件读数据四、问题总结shm_write.c:(.text0x18): undefined re…...

做出改变:农业科技和区块链在为地球的未来而战中的力量
到2050年,全球有100亿人需要养活,全世界都在关注区块链和农业信息化,以推动发展中国家的技术革新。 自成立以来,区块链技术已经找到了多样化和有价值的应用,以帮助提高效率和激励社区在不同领域和行业的参与。 农业是…...

树莓派介绍
文章目录一.树莓派介绍二.树莓派分类一.树莓派介绍 树莓派,(英语:Raspberry Pi,简写为RPi,别名为RasPi / RPI)是为学习计算机编程教育而设计,只有信用卡大小的微型电脑,其系统基于L…...

[神经网络]基干网络之VGG、ShuffleNet
一、VGG VGG是传统神经网络堆叠能达到的极限深度。 VGG分为VGG16和VGG19,其均有以下特点: ①按2x2的Pooling层,网络可以分成若干段 ②每段之内由若干same卷积操作构成,段内Feature Map数量固定不变; ③Feature Map按2的…...

Java 日期时间与正则表达式,超详细整理,适合新手入门
目录 1、java.time.LocalDate类表示日期; 2、java.time.LocalTime类表示时间; 3、java.time.LocalDateTime类表示日期和时间; 4、java.time.format.DateTimeFormatter类用于格式化日期和时间; 5、创建正则表达式对象 6、匹配…...

用Netty实现物联网04:自定义通信协议
上一讲咱们澄清了Netty的一些基本概念,然后也写了一个服务端与客户端通信的简单应答程序。从这一讲开始,就来一步步搭建一个Netty物联网应用。 大多数硬件电子产品,都自带了嵌入式软件,或者说固件。这些嵌入式软件/固件基本上都是用C/C++编写的。由于这些小微电子设备资源极…...

「smardaten」上架钉钉应用中心!让进步再一次发生
使用钉钉的团队小伙伴们,smardaten给您送来福利啦~为了给更多团队提供更优质的应用开发体验,方便用户在线、快速使用无代码,数睿数据近期在【钉钉应用中心】发布smardaten在线版本。继与华为云、亚马逊云建立战略合作之后,smardat…...

3、Maven安装
前言:工具下载地址阿里云盘:Maven:https://www.aliyundrive.com/s/SgHKjQ5doSp提取码: ml40一、什么是maven?Apache Maven是个项目管理和自动构建工具,基于项目对象模型(POM)的概念。作用:完成…...

tkinter
# 隐藏控件 tl.pack_forget() tb.pack_forget() # 显示控件 tl.pack() tb.pack() 如果您使用 grid 布局管理器,则可以使用 grid_remove() 方法将控件隐藏,使用 grid() 方法将控件显示。例如: # 隐藏控件 tl.grid_remove() tb.grid_remove() #…...

Servlet笔记(6):HTTP状态码
1、状态码 代码消息描述100 Continue只有请求的一部分已经被服务器接收,但只要它没有被拒绝,客户端应继续该请求。101 Switching Protocols服务器切换协议。200 OK请求成功。201 Created该请求是完整的,并创建一个新的资源。202 Accepted该请…...

RocketMQ 延迟队列
什么是延迟队列指消息发送到某个队列后,在指定多长时间之后才能被消费。应用场景RocketMQ 延迟队列定时消息(延迟队列)是指消息发送到broker后,不会立即被消费,等待特定时间投递给真正的topic。broker有配置项messageD…...

【精准计时】北斗GPS卫星时钟同步改变精准计时年代
【精准计时】北斗GPS卫星时钟同步改变精准计时年代 【精准计时】北斗GPS卫星时钟同步改变精准计时年代 北斗GPS成精确计时先锋 北斗GPS精确时间自动校准技术,是一种简便的获取北斗GPS精确时间信息的专利技术,具有灵敏度高、不受时间及地域限制等特点…...

【C#基础】C# 面向对象编程
序号系列文章5【C#基础】C# 运算符总结6【C#基础】C# 常用语句讲解7【C#基础】C# 常用数据结构文章目录前言面向对象的 C#1,类的概念2,类的定义3,类成员4,对象5,继承6,多态性结语前言 😊大家好&…...

数据结构与算法入门
目录数据结构概述逻辑结构存储结构算法概述如何理解“大O记法”时间复杂度空间复杂度数据结构概述 数据结构可以简单的理解为数据与数据之间所存在的一些关系,数据的结构分为数据的存储结构和数据的逻辑结构。 逻辑结构 集合结构:数据元素同属于一个集…...

【OpenAI】基于 Gym-CarRacing 的自动驾驶练习项目 | 路径训练功能的实现 | GYM-Box2D CarRacing
限时开放,猛戳订阅! 👉 《一起玩蛇》🐍 💭 写在前面: 本篇是关于多伦多大学自动驾驶专业项目的博客。GYM-Box2D CarRacing 是一种在 OpenAI Gym 平台上开发和比较强化学习算法的模拟环境。它是流行的 Box2…...

亚马逊、沃尔玛测评自养号测评、退款、撸卡撸货怎么做?
大家好,有很多的测评工作室做亚马逊测评、沃尔玛测评自养号大额退款,撸卡撸货的找到我,问我有什么方式可以解决成本,效率,纯净度,便捷性等问题,测评养号系统从最早的模拟器,虚拟机到…...

Apollo 2.1.0最新版docker 部署多环境 与java spring boot 接入demo (附带一键部署脚本)
最新Apollo 版本发布2.1.0 https://www.apolloconfig.com/#/zh/design/apollo-design 环境说明 ecs 主机一台数据库mysql 8.0docker 环境 apollo 是内网可信应用,最好是部署在内网里面,外网不可使用,避免配置信息泄漏,这里为了方…...

分布式算法 - 一致性Hash算法
一致性Hash算法是个经典算法,Hash环的引入是为解决单调性(Monotonicity) 的问题;虚拟节点的引入是为了解决 平衡性(Balance) 问题。一致性Hash算法引入在分布式集群中,对机器的添加删除,或者机器故障后自动脱离集群这些操作是分布…...

OAuth2.0入门
什么是OAuth2.0 OAuth(Open Authorization)是一个关于授权(authorization)的开放网络标准,允许用户授权第三方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方移动应用或…...

【HTTP——了解HTTP协议及状态码】
一, 什么是通信通信,就是信息的传递和交换。通信三要素:通信的主体,通信的内容,通信的方式现实生活中的通信:我打电话叫小明来我家吃饭【其中通信的主体是,我,小明。通信内容是&…...

骨传导耳机靠谱吗,骨传导耳机的原理是什么
很多人刚开始接触骨传导耳机时都会具有一个疑问,骨传导耳机是不是真的靠谱,是不是真的不伤害听力?骨传导耳机传输声音的原理是什么? 下面就给大家讲解一下骨传导耳机传输声音的原理以及骨传导耳机对听力到底有没有伤害。 骨传导…...

对个人博客系统进行web自动化测试(包含测试代码和测试的详细过程)
目录 一、总述 二、登录页面测试 一些准备工作 验证页面显示是否正确 验证正常登录的情况 该过程中出现的问题 验证登录失败的情况 关于登录界面的总代码 测试视频 三、注册界面的自动化测试 测试代码 过程中出现的bug 测试视频 四、博客列表页测试(…...

[ 2204听力 ] 五
[ 第五次课 对话1 ] Narrator Listen to a conversation between a student and her Ecology professor (woman) Hi, professor, did you want to talk about my paper? I didn’t get a grade. (man) Ah, yes, I think you might have done the wrong assignment. assign…...

嵌入式常问问题和知识
12、并发和并行的区别? 最本质的区别就是:并发是轮流处理多个任务,并行是同时处理多个任务。 你吃饭吃到一半,电话来了,你一直到吃完了以后才去接,这就说明你不支持并发也不支持并行。 你吃饭吃到一半&…...

【数据结构】空间复杂度
🚀write in front🚀 📜所属专栏:初阶数据结构 🛰️博客主页:睿睿的博客主页 🛰️代码仓库:🎉VS2022_C语言仓库 🎡您的点赞、关注、收藏、评论,是对…...

湖南中创教育提醒校外培训留意这几点,避免维权
校外教育培训机构是市场经济发展的必然产物,有需求就有市场,这个无可厚非。而校外教育培训机构的火热,正是反映出人民群众对教育发展的需求在不断增强。 培训机构分类中有面对大学生参加公务员招考、教师考编等考证考试的培训机构࿱…...

docker 配置私有/本地镜像仓库
docker 配置私有/本地镜像仓库docker pull registry mkdir -p /usr/local/docker/registry-data docker tag registry 192.168.28.132:5000/registry docker run -di -p 5000:5000 --namelocal_registry --restartalways --privilegedtrue --log-drivernone -v /usr/local/d…...

每日学术速递2.23
Subjects: Robotics 1.On discrete symmetries of robotics systems: A group-theoretic and data-driven analysis 标题:关于机器人系统的离散对称性:群论和数据驱动分析 作者:Daniel Ordonez-Apraez, Mario Martin, Antonio Agudo, F…...

LeetCode 232. 用栈实现队列
LeetCode 232. 用栈实现队列 难度:easy\color{Green}{easy}easy 题目描述 请你仅使用两个栈实现先入先出队列。队列应当支持一般队列支持的所有操作(pushpushpush、poppoppop、peekpeekpeek、emptyemptyempty): 实现 MyQueueM…...