再读linux驱动程序设计

1. 编译内核

1.1 安装内核man手册

1.1.1 配置编译内核

make menuconfig

执行后直接退出

make -j 4

make modules

make modules_install INSTALL_MOD_PATH=./build //安装模块

1.1.2 生成内核函数man手册

sudo pip install -U Sphinx

sudo pip install sphinx_rtd_theme

make installmandocs

 

1.2 helloworld

1.2.1 helloworld.c

#include 
#include 

static int __init init_func(void)
{
    printk(KERN_DEBUG "hello world!\n");

    return 0;
}

static void __exit exit_func(void)
{
    for(int index = 0; index < 10; index++);
    
    printk(KERN_DEBUG "baybay!");
}

MODULE_LICENSE("GPL");

module_init(init_func);
module_exit(exit_func);

1.2.2 makefile

ifeq ($(KERNELRELEASE),)

KERNELDIR = /home/chy/work/linux-4.15
CURRENTPATH = $(shell pwd)

defaule:
	make -C $(KERNELDIR) M=$(CURRENTPATH) modules

clean:
	make -C $(KERNELDIR) M=$(CURRENTPATH) clean
	rm -rf ./*.ko *.o

.PHONY: all clean

else
obj-m := helloworld.o
helloworld-objs := hello_world.o
ccflags-y := -std=gnu99

endif

此makefile会执行两次,第一次进入时KERNELRELEASE没有定义(KERNELRELEASE在内核顶层的makefile中定义)则会执行else下面的内容。-C切换到内核顶层目录,-M表示在执行modules目标之前,返回源代码目录并执行makefile,此时KERNELRELEASE已被定义,将读取else之前的内容,设置obj-m的值,内核模块的makefile将负责真正构造模块。modules目标指向obj-m变量中设定的模块。

 

2. 驱动的加载和卸载

2.1 insmod 和 modprode

加载驱动程序,前者只加载指定的驱动程序,后者检查并加载指定驱动程序的依赖。

2.2 rmmod

卸载指定的驱动模块。

2.3 lsmod

查看内核中的驱动模块。

 

3. 内核符号表的导出和模块参数的传递

3.1 内核符号表的导出

EXPORT_SYMBOL(name)

EXPORT_SYMBOL_GPL(name)

两者都是导出内核符号表,后者导出的内核符号表只能被遵循GPL协议的模块使用。

3.2 模块参数的传递

module_param(name, type, perm)

module_param_array(name, num, type, perm)

这两个宏在moduleparam.h中定义,前者导出非数组类型,后者可以导出数组类型。name表示变量名称,type表示要导出的类型,prem访问权限, num数组的长度。

内核支持模块参数的类型有:

bool、invbool(bool取反)、charp(字符指针)、int、long、short、uint、ulong、ushort。

 

4. 预备知识

4.1 module_init(init_func)

static int __init init_func()

{

//驱动的初始化

}

module_init(init_func);

 

module_init()宏是必须的,它在求驱动初始化函数中添加特殊的段,用于说明驱动初始化函数的位置,不然驱动程序永远得不到执行。__init暗示该函数为驱动初始化函数,在模块被装载后,模块装载器就会把该函数的内存释放。

4.2 module_exit(exit_func)

static int __exit exit_func()

{

//驱动的清理

}

module_ exit(exit_func);

module_exit()宏对帮助内核找到驱动清除函数是必须的。

4.3 MODULE_LICENSE()

该宏指定模块使用的许可证。

4.4 内核空间和用户空间数据交换

unsigned long copy_to_user(void __user *to, const void *from, unsigned long n) //从内核空间到用户空间

unsigned long copy_from_user(void *to, const void __user *from, unsigned long n) //从用户空间到内核空间

 

5. 字符设备驱动程序

5.1 设备号

MAJOR(dev_t dev) 从dev_t 获取主设备号

MINOR(dev_t dev) 从dev_t 获取次设备号

MKDEV(int major, int minor) //从主次设备号生成dev_t

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

获取设备编号,first第一个设备的设备号,count总共几个这样的设备,name是和该编号的关联的设备名称。

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

动态获取设备编号,dev为内核分配的设备编号,firstminor第一个次设备号,count连续设备编号的个数,name和改编号关联的设备名称。

void unregister_chrdev_region(dev_t first, unsigned int count);

释放设备编号

5.2 重要的数据结构

struct file_operations //文件的具体操作

struct file //打开文件的描述

struct inode //内部文件结构

 

unsigned int iminor(struct inode *node) //从inode获取次设备号

unsigned int imajor(struct inode *node) //从inode获取主设备号

5.3 字符设备的注册

struct cdev //描述一个字符设备

void cdev_init(struct cdev *cdev, struct file_operations *fops) //初始cdev

int cdev_add(struct cdev *cdev, dev_t num, unsigned int count) //告诉内核该结构信息

void cdev_del(struct cdev *cdev) //删除一个字符设备

 

6. 并发和竞争

6.1 信号量和互斥体

6.1.1 信号量

void sema_init(struct semaphore *sema, int val); //信号量初始化

void down(struct semaphore *sema); //获取一个信号量,如果获取不到一直等待,会造成进程的睡眠。

int down_interruptible(struct semaphore *sema); //获取一个信号量,可以被中断打断。

int down_trylock(struct semaphore *sema); //尝试获取一个信号量,立即返回

void up(struct semaphore *sema); 释放一个信号量

6.1.2 互斥体

一种特殊的信号量

DECLARE_MUTEX(name) //编译时初始化一个信号量

DECLARE_MUTEX_LOCKED(name) //编译时初始化一个信号量,并处与锁定状态

void init_mutex(struct semaphore *sema) //运行时初始化信号量

void init_mutex_locked(struct semaphore *sema) //运行时初始化信号量,并处与锁定状态

获取互斥体、释放互斥体与操作信号量调用的函数一样。

6.2 基于信号量的读写锁

void init_rwsem(struct rw_semaphore *sema) //初始化基于信号量的读写锁

 

void down_read(struct rw_semaphore *sema) //以读锁的方式获取基于信号量

int down_read_trylock(struct rw_semaphore *sema) //以读锁的方式获取基于信号量,立即返回

void up_read(struct rw_semaphore *sema) //释放读锁定的锁

 

void down_write(struct rw_semaphore *sema) //以写锁的方式获取基于信号量

int down_write_trylock(struct rw_semaphore *sema) //以写锁的方式获取基于信号量,立即返回

void up_write(struct rw_semaphore *sema) //释放写锁定的锁

void downgrade_write(struct rw_semaphore *sema) //

6.3 自旋锁

spinlock_t lock = SPIN_LOCK_UNLOCKED //编译时初始化自旋锁

void spin_lock_init(spin_lock_t *lock); //运行期间初始化自旋锁

void spin_lock(spin_lock_t *lock); //获取自旋锁,在持有自旋锁期间内核的抢占被禁止

void spin_lock_irqsave(spin_lock_t *lock, unsigned long f); //获取自旋锁并禁止中断(只在本地处理器中),中断状态保存在中。

void spin_lock_irq(spin_lock_t *lock); //禁止中断

void spin_lock_hb(spin_lock_t *lock); //禁止软中断

void spin_unlock(spin_lock_t *lock); //释放自旋锁

*用有自旋所期间禁止睡眠

6.4 基于自旋锁的读写锁

rwlock_t lock = RW_LOCK_UNLOCKED //编译期间初始化读写锁

rwlock_init(rwlock_t *rwlock); //运行期间初始化自旋锁

void read_lock_irq(rwlock_t *lock);//以读锁的方式获取读写锁

void read_lock_irqsave(spin_lock_t *lock, unsigned long f); //以读锁的方式获取读写锁并禁止中断

void read_lock_bh(spin_lock_t *lock)//以读锁的方式获取读写锁并禁止软中断

void read_lock(spin_lock_t *lock) //以读锁的方式获取读写锁

 

void read_unlock_irq(rwlock_t *lock);//释放以读锁的方式获取读写锁

void read_unlock_irqsave(spin_lock_t *lock, unsigned long f); //释放以读锁的方式获取读写锁并禁止中断

void read_unlock_bh(spin_lock_t *lock)//释放以读锁的方式获取读写锁并禁止软中断

void read_unlock(spin_lock_t *lock) //释放以读锁的方式获取读写锁

 

void write_lock_irq(rwlock_t *lock);//以写锁的方式获取读写锁

void write_lock_irqsave(spin_lock_t *lock, unsigned long f); //以写锁的方式获取读写锁并禁止中断

void write_lock_bh(spin_lock_t *lock)//以写锁的方式获取读写锁并禁止软中断

void write_lock(spin_lock_t *lock) //以写锁的方式获取读写锁

 

void write_unlock_irq(rwlock_t *lock);//释放以写锁的方式获取读写锁

void write_unlock_irqsave(spin_lock_t *lock, unsigned long f); //释放以写锁的方式获取读写锁并禁止中断

void write_unlock_bh(spin_lock_t *lock)//释放以写锁的方式获取读写锁并禁止软中断

void write_unlock(spin_lock_t *lock) //释放以写锁的方式获取读写锁

6.5 完成量

DECLARE_COMPLETION(name) //编译时初始化

init_completion(struct completion *com) //运行时初始化

void wait_for_completion(struct completion *com) //等待completion ,不能被中断

void completion(struct completion *com) //唤醒一个在等待的进程

void completion_all(struct completion *com) //唤醒所有在等待的进程

INIT_COMPLRETION(struct completion c) //执行completion_all后必须重新初始化completion

6.6 原子操作

6.6.1 原子变量

atomic_t ATOMIC_INIT // 编译期间初始化

void atomic_set(atomic_t *v, int i)

int atomic_read(atomic_t *v)

void atomic_add(int i, atomic_t *v)

void atomic_sub(int i, atomic_t *v)

void atomic_inc(atomic_t *v)

void atomic_dec(atomic_t *v)

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)

int atomic_inc_and_test(int i, atomic_t *v)

int atomic_dec_and_test(int i, atomic_t *v)

int atomic_sub_and_test(atomic_t *v)

int atomic_add_negative(atomic_t *v)

6.6.2 位操作

void set_bit(int i, void *addr);

void clear_bit(int i, void *addr);

void change_bit(int i, void *addr);

test_bit(int i, void *addr);

int test_and_set_bit(int i, void *addr);

int test_and_clear_bit(int i, void *addr);

int test_and_change_bit(int i, void *addr);

 

7. 时间缓存及延时操作

7.1 HZ和jiffies

HZ系统时钟中断频率

jiffies系统从最近启动开始系统时钟中断的次数,为32位数据长度

7.2 获取当前时间

unsigned long mktime(const unsigned int year, const unsigned int mon,

const unsigned int day, const unsigned int hour,

const unsigned int min, const unsigned int sec)

时钟墙转换为jiffies

7.3 长延时

长延时指常于一个时钟滴答,长延时的方法通常有忙等待、让出处理器、超时

7.4 短延时

void ndelay(unsigned long nsces)

void udelay(unsigned long usces)

void mdelay(unsigned long msces)

以上三个函数均为忙等待

void msleep(unsigned int msecs); //睡眠指定的时间,不可中断

unsigned long msleep_interruptible(unsigned int msecs);//睡眠指定的时间,可中断

void ssleep(unsigned int ssecs);//睡眠指定的时间,不可中断

7.5 内核定时器

DEFINE_TIMER(_name, _function) //编译期间初始化

void add_timer(struct timer_list *timer); //注册一个定时器

int del_timer(struct timer_list * timer); //删除一个定时器

int mod_timer(struct timer_list *timer, unsigned long expires); //更新某个到期的定时器

int del_timer_sync(struct timer_list *timer); //与del_timer类似,但其确保返回时没有cpu在运行定时器器。

int timer_pending(const struct timer_list * timer) //定时器是否被调用

7.6 tasklet

始终在中断期间运行,始终在调度它的cpu上运行,以原子模式执行,始终不会晚于下一个定时器滴答执行,不可睡眠。

void tasklet_init(struct tasklet_struct *t, void (*func)(unsigned long), unsigned long data);

DECLARE_TASKLET(name, func, data)

DECLARE_TASKLET_DISABLED(name, func, data)

void tasklet_disable(struct tasklet_struct *t)

void tasklet_disable_nosync(struct tasklet_struct *t) //禁止tasklet,不等待结束,立即返回

void tasklet_enable(struct tasklet_struct *t)

void tasklet_kill(struct tasklet_struct *t);

void tasklet_schedule(struct tasklet_struct *t)

void tasklet_hi_schedule(struct tasklet_struct *t) //以高优先级的方式调度

*启用和禁止的次数相同才能被执行。

7.7 工作队列

struct workqueue_struct *create_workqueue(const char *name) //创建工作队列的宏,在每个处理器上创建

struct workqueue_struct *create_singlethread_workqueue(const char *name) //在单个的处理器中创建

void destroy_workqueue(struct workqueue_struct *wq);//销毁工作队列

DECLARE_WORK(name, func)//编译期间初始化

INIT_WORK(_work, _func)//运行期间初始化

bool queue_work(struct workqueue_struct *wq, struct work_struct *work) //添加到工作队列

bool queue_delayed_work(struct workqueue_struct *wq, struct delayed_work *dwork, unsigned long delay)//经过delay个jiffies后添加到队列

bool cancel_work_sync(struct work_struct *work);//入口项在开始执行前被取消,返回0则表明其已经在其他处理器上运行。

bool flush_work(struct work_struct *work);//任何之前提交的工作函数都不会再系统中执行

7.8 共享队列

在驱动程序不需要使用自己的工作队列时可以使用内核默认创建的共享的工作队列。

bool schedule_work(struct work_struct *work) //调度工作队列执行

bool schedule_delayed_work(struct delayed_work *dwork, unsigned long delay); //等待delay个jiffies后调度执行

7.9 等待队列(wait.h)

DECLARE_WAIT_QUEUE_HEAD(name) //编译期间初始化等待队列头部

init_waitqueue_head(wq_head)//运行期间初始化等待队列头部

wait_event(wq_head, condition)//等待条件满足

wait_event_interruptible(wq_head, condition)//等待条件满足,可以被中断

wait_event_timeout(wq_head, condition, timeout)//等待条件满足,带超时

wait_event_interruptible_timeout(wq_head, condition, timeout)//等待条件满足,可以被中断,带超时

wake_up(struct wait_queue_head *wq_head) //唤醒等待队列的所有进程

wake_up_interruptible(struct wait_queue_head *wq_head)//唤醒等待队列上,可被中断的所有进程

wake_up_nr(x, nr) //唤醒nr个独占等待

wake_up_interruptible_nr(x, nr) //唤醒nr个独占等待,可被中断

wake_up_all(x) //唤醒独占和非独占

wake_up_interruptible_all(x)//唤醒独占和非独占

 

DEFINE_WAIT(name) //初始化等待队列的入口项

DEFINE_WAIT_FUNC(name, function)//初始化等待队列的入口项

init_wait(wait) //初始化等待队列的入口项

void prepare_to_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state);

把等待队列的入口项添加到等待队列,state进程的新状态(TASK_INTERRUPTIBLU或TASK_UNINTERRUPTIBLU),通常为可中断的睡眠TASK_INTERRUPTIBLU,也可以为TASK_UNINTERRUPTIBLU。在调用该函数后通常条用schedule()切换进程的执行。

void prepare_to_wait_exclusive(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry, int state);//独占

void finish_wait(struct wait_queue_head *wq_head, struct wait_queue_entry *wq_entry);

用于结束wait_queue 之后的清理工作,首先将当前函数的状态设置为TASK_RUNNING,如果第二个形参wait->task_list 不为null的话,则将wait->task_list 做置null,并重新对wait->task_list进行初始化。

7.10 jiffies的比较

int time_after(a, b) // 如果a代表的时间比b靠后为真

int time_before(a, b) // 如果a代表的时间比b靠前为真

int time_after_eq(a, b) // 如果a代表的时间比b靠后或相等为真

int time_before_eq(a, b) // 如果a代表的时间比b靠前或相等为真

上面四个宏用来比较jiffies的值

 

8. 分配内存

8.1 kmalloc (linux/slab.h)

void *kmalloc(size_t size, gfp_t flags)

void kfree(const void *);

size: 将要分配的内存大小,如果分配任意大小的内存,可能会比请求的多一些,最多多出申请数量的两倍。

flags:内存分配的行为。

GFP_ATOMIC:

用于中断处理程序或处进程上下文之外的内存分配,不会睡眠。

GFP_KERNEL

内核内存通用的分配方法,会引起睡眠。

GFP_USER

为用户进程分配空间,可能会引起睡眠。

GFP_HIGHUSER

如果存在高端内存则分配高端内存。

8.2 vmalloc

void *vmalloc(unsigned long size);//分配内存

void vfree(const void *addr); //释放

void *ioremap(phys_addr_t phys_addr, size_t size);//完成从物理内存到虚拟内存的映射

void iounmap(void *addr);//去除映射

*分配的内存虚拟地址空间连续,物理地址空间可能不连续。

8.3 后备高速缓存

struct kmem_cache *kmem_cache_create(const char *name, unsigned int size,

unsigned int align, slab_flags_t flags, void (*ctor)(void *));

创建后备高速缓存对象。

name与缓存关联的名称,size缓存区域的大小,align在第一个页面中的偏移量(通常为0),flags控制如何分配,ctor分配内存对象时调用。

void *kmem_cache_alloc(struct kmem_cache *, gfp_t flags)//分配内存对象时间调用

void kmem_cache_free(struct kmem_cache *, void *);//释放内存对象时调用

void kmem_cache_destroy(struct kmem_cache *); //销毁后备高速缓存对象。

8.4 内存池

mempool_t *mempool_create(int min_nr, mempool_alloc_t *alloc_fn, mempool_free_t *free_fn, void *pool_data);

创建内存池,min_nr保存内存池创建已经分配对象的最少数目,alloc_fn创建对象,free_fn释放对象,pool_data传递给alloc_fn和free_fn的参数。

void *mempool_alloc_slab(gfp_t gfp_mask, void *pool_data);

void mempool_free_slab(void *element, void *pool_data);

alloc_fn和free_fn通常使用mempool_alloc_slab和mempool_free_slab

int mempool_resize(mempool_t *pool, int new_min_nr);//调整内存池的大小

void mempool_destroy(mempool_t *pool); //销毁内存池

void *mempool_alloc(mempool_t *pool, gfp_t gfp_mask); //调用分配函数分配对象,如果失败则从内存池获取已经分配的对象

void mempool_free(void *element, mempool_t *pool);//如果预先分配的数目小于设置的最小值则保存在内存池,否则返回给系统。

8.5 get_free_page

unsigned long get_zeroed_page(gfp_t gfp_mask); //获取一页不清零

unsigned long __get_free_pages(gfp_t gfp_mask, unsigned int order); //获取2^order个页不清零

unsigned long get_zeroed_page(gfp_t gfp_mask); //获取一页并清零

void free_pages(unsigned long addr, unsigned int order); //释放2^order个页

free_page(addr) //释放1页

*如果使用的内存较大,使用面向分配分配技术会更好。

8.6 alloc_pages(gfp.h)

struct page *alloc_pages_node(int nid, gfp_t gfp_mask, unsigned int order)

struct page *alloc_pages(gfp_mask, order)

struct page *alloc_page(gfp_mask)

nid NUMA的ID号表示在其中分配内存,gfp_mask分配方式,order分配大小,page 指向第一个分配的page的信息失败为NULL。

void __free_pages(struct page *page, unsigned int order);

void __free_page(struct page *page)

*内核管理系统的物理内存,内存只能按页面进行分配。linux内存分配方式是创建一系列的内存对象池,每个池的内存块大小是固定的,在请求内存时直接在包含足够大的池中传递一个整块给请求者。内存分为DMA内存,高端内存,常规内存。

*slab就是内核维护的一组大小不同的空闲链表,Linux把每个空闲链表称为后备高速缓存(lookaside cache),当需要使用时,可以从中取出需要的内存块,不用时,再把内存块归还给slab。

*内核有些地方不允许内参分配失败,内核开发者建立了内存池的机制,内存池是某种形式后备高速缓存,它始终持有空闲内存。

 

9. i/o操作

9.1 i/o端口和i/o内存

每种外设都通过读写寄存器进行控制。大部分的外设都有几个寄存器,不管是内存地址空间还是在IO地址空间,这些寄存器地址的访问都是连续的。

硬件层,内存区域和I/O区域没有概念上的区别,他们都是通过地址总线和控制总线发送电平信号进行访问,再通过数据总线读写数据。

一些CPU厂商使用统一地址空间(ARM),另一些则使用独立的地址空间(x86)。

因为外设要与外围总线相匹配,而最流行的I/O总线是基于个人计算机模型的,所以即使原来没有独立的I/O端口地址空间的处理器,在访问外设的时间也要模拟成读写I/O端口,这通常是由外部芯片组或CPU内心附加电路来实现。

9.2 I/O寄存器和常规内存

I/O寄存器和RAM的主要区别是I/O操作具有边际效应(副作用),而内存的操作没有。编译器的优化,CPU的乱序执行等对I/O寄存器操作可能有致命的错误。

对硬件自身缓存的引起的问题:只需要把低层硬件配置成在访问I/O区域时禁止硬件缓存即可。对于编译器优化和硬件重新排序引起的问题,对硬件必须顺序执行的操作设置内存屏障。

9.3 内存屏障

void barrier(void)

通知编译器插入一个内存屏障,但对硬件没有影响。编译后的代码会把当前cpu寄存器修改的数据保存到内存,需要这些数据的时间在从新读取出来,对barrier的调用可以避免在屏障前后的编译器优化,但硬件能完成自己的重新排序。

void rmb(void)

void read_barrier_depends(void)

void wmb(void)

void mb(void)

这些函数在已经编译的指令流中插入硬件内存屏障;rmb(读内存屏障)保证了屏障之前的读操作一定会在后来的读操作执行前完成。wmb保证写操作不会乱序,mb保证读写都不会。

void smp_rmb(void)

void smp_read_barrier_depends(void)

void smp_wmb(void)

void smp_mb(void)

上面四个宏仅对SMP系统编译时有效,在单处理器系统上,他们均被扩张为最上面简单的屏障。

9.4 i/o端口的访问

struct resource *request_region(unsigned long start, unsigned long n, const char *name)

获取 起始于start的n个端口,name设备名称

void release_region(unsigned long start, unsigned long n)

释放 起始于start的n个端口

 

字节读取端口:

u8 inb(unsigned long addr)

void outb(u8 value, unsigned long addr)

16bit读取端口:

u16 inw(unsigned long addr)

void outw(u16 value, unsigned long addr)

32bit访问端口:

u32 inl(unsigned long addr)

void outl(u32 value, unsigned long addr)

串操作,在addr位置连读/写count个数据(8,16,32位的数据):

void insb(unsigned long addr, void *buffer, unsigned int count)

void outsb(unsigned long addr, const void *buffer, unsigned int count)

void insw(unsigned long addr, void *buffer, unsigned int count)

void outsw(unsigned long addr, const void *buffer, unsigned int count)

void insl(unsigned long addr, void *buffer, unsigned int count)

void outsl(unsigned long addr, const void *buffer, unsigned int count)

9.5 i/o内存的访问

struct resource *request_mem_region(unsigned long start, unsigned long n, const char *name)

获取起始地址为start,长度为n的I/O内存资源,name为设备名

void release_mem_region(unsigned long start, unsigned long n)

释放起始地址为start,长度为n的I/O内存资源

 

u8 ioread8(const volatile void *addr)

u16 ioread16(const volatile void *addr)

u32 ioread32(const volatile void *addr)

u64 ioread64(const volatile void *addr)

void iowrite8(u8 value, volatile void __iomem *addr)

void iowrite16(u16 value, volatile void __iomem *addr)

void iowrite32(u32 value, volatile void __iomem *addr)

void iowrite64(u64 value, volatile void __iomem *addr)

 

void iowrite8_rep(volatile void __iomem *addr, const void *buffer, unsigned int count)

void iowrite16_rep(volatile void __iomem *addr, const void *buffer, unsigned int count)

void iowrite32_rep(volatile void __iomem *addr, const void *buffer, unsigned int count)

void iowrite64_rep(volatile void __iomem *addr, const void *buffer, unsigned int count)

void ioread8_rep(const volatile void __iomem *addr, void *buffer, unsigned int count)

void ioread16_rep(const volatile void __iomem *addr, void *buffer, unsigned int count)

void ioread32_rep(const volatile void __iomem *addr, void *buffer, unsigned int count)

void ioread64_rep(const volatile void __iomem *addr, void *buffer, unsigned int count)

 

在一块I/O内存上执行操作

void memset_io(volatile void __iomem *addr, int value, size_t size)

void memcpy_fromio(void *buffer, const volatile void __iomem *addr, size_t size)

void memcpy_toio(volatile void __iomem *addr, const void *buffer, size_t size)

 

10. 中断

10.1 中断资源的申请(linux/interrupt.h)

int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev)

irq:请求的中断线号。

handler:中断处理程序。

flags:中断管理相关的位掩码。

IRQF_SHARED:共享中断。

IRQF_DISABLED:快速中断处理。

name:表明中断的拥有者。

dev:用于共享的中断信号线。它是唯一的标识符,在中断空闲的时可以使用它,也可以只想中断程序的私有数据区。

void *free_irq(unsigned int irq, void *dev);

irq:请求的中断线号。

dev:用于共享的中断信号线。它是唯一的标识符。

10.2 中断的探测

unsigned long probe_irq_on(void)

int probe_irq_off(unsigned long val)

只能用在非共享模式下,probe_irq_on改函数返回一个中断掩码,调用该函数后驱动程序将安排产生至少一次中断。probe_irq_off函数将返回刚才产生中断的中断号。

10.3 中断处理程序

irqreturn_t (*irq_handler_t)(int irq, void *dev)

注册的回调函数,其回调函数表明中断是否被处理,如果中断处理程序发现是本设备引起的则应该返回IRQ_HANDLED,否则返回IRQ_NONE。

10.4 中断的禁止和启用

void disable_irq(int irq) //禁止irq中断,并等待irq中断处理程序执行完后返回

void disable_irq_nosync(int irq) //禁止irq中断,立即返回

void enable_irq(int irq) //使能irq

 

void local_irq_save(unsigned long f); //禁止本处理器的中断,并保存中断信息到f

void local_irq_disable(); //禁止本处理器的中断

void local_irq_restore(unsigned long f); //使能本处理器的中断,并从f中恢复中断信息

void local_irq_enable(); //使能本处理器的中断

10.5 顶半部中断和低半部中断

顶半部中断是实际响应中断的程序,即通过request_irq注册的处理程序。而低半部是被顶半部调用的在稍后安全时间执行的例程。顶半部和低半部最大的不同是,低半部执行时所有的中断是开的(安全时间)。经典的做法是顶半部保存设备的数据到缓存区,然后低半部执行必要的工作。低半部中断可以使用taskle和workqueue来实现。

taskle:可以由系统决定的安全时期在软中断上下文中调度执行的特殊函数。它们可以被多次调度运行,但是不会积累。不会有一个tasklet的多个实例并行运行,因为tasklet只能被调度执行一次,但不同的tasklet可以在SMP处理器上并行的运行。

workqueue:运行在进程上下文中可以睡眠。

10.6 中断共享

在共享中断中注册(request_irq)时dev必须唯一。

11. 总线设备驱动模型

 

12. 平台总线和混杂设备

 

13. usb驱动

 

14. 块设备驱动

 

15. 内存映射

linux是一个虚拟内存系统,用户进程使用的地址和硬件使用的物理地址是不等同的。

15.1 地址类型

(1) 用户虚拟地址:

用户空间程序所能看到的常规地址。

(2)物理地址

改地址在处理器和系统内存之间使用。

(3)总线地址

改地址在外围总线和系统内存之间使用。通常他们和处理其使用的物理地址相同,但这么做并不必须。

(4)内核逻辑地址

内核逻辑地址组成了内核的常规地址空间。该地址映射了部分或者全部的内存,并经常被视为物理内存地址。大多数的体系架构中,逻辑地址和与其关联的物理地址存在一个固定的偏移量。kmallc返回的是逻辑地址。

(5)内核虚拟地址

内核的虚拟地址和内核的逻辑地址的相同之处在于,它们都将内核的地址空间的地址映射到物理地址上。但是内核的虚拟地址与物理地址的映射不必是线性的和一对一的,而这是逻辑地址的特性。

15.2 物理地址和页

物理地址被分成离散的单元,称之为页。系统内部对内存的操作都是基于单个的页。

15.3 高端内存和低端内存

(1)低端内存

存在于内核空间的逻辑地址内存。

(2)高端内存

指那些不存在逻辑地址的内存,处于内核的虚拟地址空间。

15.4 内存映射和页结构

内核使用逻辑地址来引用物理内存中的页,然而由于支持了高端内存,就暴露了一个明显的问题--在高端地址中无法使用逻辑地址,因此内核中处理内存的函数趋向于使用指向page结构的指针。

page结构的几个成员:

atomic_t count //对该页的访问计数,当计数指为0时返回给空闲链表。

void *virtual //如果页面被映射,则指向页的内核虚拟地址;如果未被映射则为NUL。低端内存页总是被映射;而高端内存页通常不被映射。

unsigned long flags //描述页状态的一系列标志。

15.5 虚拟地址和page之间使用函数或宏进行转换。

struct page *virt_to_page(void *kaddr) //将内核逻辑地址转换为page结构。

struct page *pfn_to_page(int pfn) //对给定的页帧号转换为对应的page结构。

void *page_address(struct page *page) //如果地址存在的话返回内核的虚拟地址。

void *kmap(struct page *page) //为系统中的页返回虚拟地址。对于低端内存返回逻辑地址。对于高端内存,kmap在专用的内核地址空间创建特殊的映射。没有映射的时间会睡眠。

void kumap(struct page *page)

void *kmap_atomic(struct page *page, enum km_type type); //kmap的高性能版本

void kumap_atomic(void *addr, enum km_type type)

15.6 页表

任何现代的系统中,处理器必须使用某种机制,将虚拟地址转换为相应的物理地址,这种集中被称为页表。

15.7 虚拟内存

虚拟内存用于管理进程地址空间中不同的内核数据。一个VMA表示在进程的虚拟内存中的一个同类的区域:拥有同样的权限标志和被同样的对象备份的一个连续的虚拟内存地址范围。进程内存映射至少包含下面的这些区域:

(1)程序的可执行代码区域。

(2)多个数据区域,其中包括初始化数据、非初始化数据、堆栈。

(3)与每个活动的内存映射的区域对应。

15.8 vm_area_struct 结构

当用户空间进程调用mmap,将设备的内存映射到它的地址空间,系统通过创建一个表示该映射的新的VMA作为响应。支持mmap的驱动程序需要帮助进程完成VMA的初始化。

unsigned long vm_start

unsigned long vm_end

VMA所覆盖的虚拟地址的范围。

struct file *vm_file

指向与该区域相关联的file结构指针。

unsigned long vm_pgoff

以页为单位,文件中该区域的偏移量。当映射一个文件或者设备时,它是该区域中被映射的第一页在文件的位置。

unsigned long vm_flags

描绘该区域的一套标志。

struct vm_operations_struct *vm_ops

内核能调用的一套函数,用来对该区域进行操作。

viod *vm_private_data

驱动程序要保存自身数据信息的成员。

 

15.8.1 struct vm_operations_struct

void (*open) (struct vm_sturct vm_area_struct *vml)

内核调用open函数,允许实现VMA的子系统初始化该区域。

void (*close)(struct vm_sturct vm_area_struct *vml)

销毁一个区域时间调用close函数操作。

struct page *(*nopage)(struct vm_area_struct *vma, unsing long addrss, int *type)

当一个进程要访问合法的VMA的页,但是该页又不在内存中时,则为相关区域调用该函数。(在虚拟地址但被加载到内存中,产生缺页中断)。

15.9 内存映射

最后一个难题是处理内存映射结构,它负责整合其他所有的数据结构。在系统中的内一个进程都拥有一个struct mm_struct 结构,其中包含了虚拟内存区域链表、页表以及大量的内存管理信息,还包含一个信号量和一个自旋锁。在task结构中能够找到该结构的指针;在少数情况上下驱动程序需要访问它,一般使用current->mm,多个进程能够共享内存管理结构。

15.10 创建新的页表

15.10.1 emap_pfn_range和io_remap_pfn_range负责为一段物理内存建立新的页表。

int remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);

int io_remap_pfn_range(struct vm_area_struct *vma, unsigned long virt_addr, unsigned long pfn, unsigned long size, pgprot_t prot);

vma 虚拟内存区域。

virt_addr 从新映射的起始用户虚拟地址。

pfn 与物理内存对应的页帧号,虚拟内存应该被映射到该物理内存上。

size 字节为单位,被重新映射区域的大小。

prot 新VMA要求的“保护”属性。

15.10.2 nopage 映射内存

有时驱动程序对mmap的实现必须具有更好的灵活行,在该情况下提倡使用VMA的nopage方法。

struct page *(*nopage)(struct vm_area_struct *vma, unsigned long addrss, int *type);

16. 块设备驱动程序

块设备主要是传输固定大小的随机数据来访问设备。

 

*内核态和用户态

内核线程运行在内核态有特权操作,用户进程运行在用户态。用户进程可以通过系统调用陷入内核态,此时内核线程代表用户进程执行。用户态和内核态保证了操作系统的安全性,用户进程的崩溃不会对操作系统产生太大的影响。

内核态,运行于进程上下文,内核代表进程运行于内核空间;

内核态,运行于中断上下文,内核代表硬件运行于内核空间;

用户态,运行于用户空间。

*在中断上下文中不能睡眠,因为无法被唤醒,整个系统将处于挂起状态。

*在持有自旋锁的进程中禁止内核抢占

*可中断和不可中断的睡眠(http://https://blog.csdn.net/lws123253/article/details/80472793 )

Linux 中的进程睡眠状态有两种:一种是可中断的睡眠状态,其状态标志位为TASK_INTERRUPTIBLE;另一种是不可中断的睡眠状态,其状态标志位为TASK_UNINTERRUPTIBLE。

可中断的睡眠状态的进程会睡眠直到某个条件变为真,如产生一个硬件中断、释放进程正在等待的系统资源或是传递一个信号都可以是唤醒进程的条件。

不可中断睡眠状态与可中断睡眠状态类似,但是它有一个例外,那就是把信号传递到这种睡眠状态的进程不能改变它的状态,也就是说它不响应信号的唤醒。

 

 

 

 

 

你可能感兴趣的:(linux驱动程序设计)