Linux驱动开发学习归纳-1
【驱动模块的组成】
1. 头文件:两个必须包含的
#include<linux/module.h>
#include<linux/init.h>
2.模块加载函数(必须):module_init(xxxxxx);
3.模块卸载函数(必须):module_exit(xxxxxx);
4.模块许可声明(必须):MODULE_LICENSE("GPL");
5.模块参数(可选):module_param( var, type, flag);、模块功能函数(可选)、其他。
【编译内核模块的条件】
1.使用正确版本的编译工具、模块工具等。
2.一份内核源码。
3.内核源码至少编译过一次。
【模块文件格式】
| ELF Header | .text | .data | 其他 | Section Table | .symtab | 其他 |
其中.symtab 表示符号表:一种映射函数到真实内存地址的数据结构。
【将模块加入内核】
内核源码的组合是通过目录结构实现的,每个目录下面都有两个文件Kconfig和Makefile。Kconfig描述了所属目录源文件相关的内核配置菜单。在执行内核配置make menuconfig命令时,从Kconfig中读出菜单,用户选择配置后保存到.config文件。在内核编译是,执行主目录下的Makefile时,调用这个.config文件,完成用户配置项的编译。
Kconfig文件的语法可查看内核Documentation/kbuild/kconfig-language.txt文件。
所以向Linux内核添加模块时,需要: 将驱动程序源文件放到Linux内核源码的相应目录中,在目录的Kconfig文件中添加对应的编译选择,在目录的Makefile中添加对应的编译语句。
此外可以动态加载卸载模块:insmod xxx.ko 、rmmod xxx.ko、modprobe。查看模块:lsmod、modinfo
【字符设备驱动】
1.主设备号和次设备号:dev_t类型表示设备号
typedef u_long dev_t;
2.分配设备号:
静态分配设备号:register_chrdev_region();
动态分配设备号:alloc_chrdev_region();
3.释放设备号:unregister_chrdev_region();
4.Linux中用cdev结构体描述字符设备:
struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};
struct list_head {
struct list_head *next, *prev;
};
内核使用inode结构体在内部表示文件,Linux中用inode结构体表示/dev/目录下的设备文件,每个字符设备在/dev/目录下都有一个设备文件,打开设备文件时,系统会产生一个inode结点,然后通过inode结点的i_cdev字段找到cdev字符结构体,通过cdev的ops指针,找到设备的操作函数。
723 struct inode {
724 struct hlist_node i_hash;
725 struct list_head i_list; /* backing dev IO list */
726 struct list_head i_sb_list;
727 struct list_head i_dentry;
728 unsigned long i_ino;
729 atomic_t i_count;
730 unsigned int i_nlink;
731 uid_t i_uid;
732 gid_t i_gid;
733 dev_t i_rdev;
734 u64 i_version;
735 loff_t i_size;
736 #ifdef __NEED_I_SIZE_ORDERED
737 seqcount_t i_size_seqcount;
738 #endif
739 struct timespec i_atime;
740 struct timespec i_mtime;
741 struct timespec i_ctime;
742 blkcnt_t i_blocks;
743 unsigned int i_blkbits;
744 unsigned short i_bytes;
745 umode_t i_mode;
746 spinlock_t i_lock; /* i_blocks, i_bytes, maybe i_size */
747 struct mutex i_mutex;
748 struct rw_semaphore i_alloc_sem;
749 const struct inode_operations *i_op;
750 const struct file_operations *i_fop; /* former ->i_op->default_file_ops */
751 struct super_block *i_sb;
752 struct file_lock *i_flock;
753 struct address_space *i_mapping;
754 struct address_space i_data;
755 #ifdef CONFIG_QUOTA
756 struct dquot *i_dquot[MAXQUOTAS];
757 #endif
758 struct list_head i_devices;
759 union {
760 struct pipe_inode_info *i_pipe;
761 struct block_device *i_bdev;
762 struct cdev *i_cdev;
763 };
764
765 __u32 i_generation;
766
767 #ifdef CONFIG_FSNOTIFY
768 __u32 i_fsnotify_mask; /* all events this inode cares about */
769 struct hlist_head i_fsnotify_mark_entries; /* fsnotify mark entries */
770 #endif
771
772 #ifdef CONFIG_INOTIFY
773 struct list_head inotify_watches; /* watches on this inode */
774 struct mutex inotify_mutex; /* protects the watches list */
775 #endif
776
777 unsigned long i_state;
778 unsigned long dirtied_when; /* jiffies of first dirtying */
779
780 unsigned int i_flags;
781
782 atomic_t i_writecount;
783 #ifdef CONFIG_SECURITY
784 void *i_security;
785 #endif
786 #ifdef CONFIG_FS_POSIX_ACL
787 struct posix_acl *i_acl;
788 struct posix_acl *i_default_acl;
789 #endif
790 void *i_private; /* fs or device private pointer */
791 };
file_operations是一个对设备进行操作的抽象结构体,它将很多函数指针集中在该结构体中。1489 struct file_operations {
1490 struct module *owner;
1491 loff_t (*llseek) (struct file *, loff_t, int);
1492 ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
1493 ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
1494 ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1495 ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
1496 int (*readdir) (struct file *, void *, filldir_t);
1497 unsigned int (*poll) (struct file *, struct poll_table_struct *);
1498 int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);
1499 long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
1500 long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
1501 int (*mmap) (struct file *, struct vm_area_struct *);
1502 int (*open) (struct inode *, struct file *);
1503 int (*flush) (struct file *, fl_owner_t id);
1504 int (*release) (struct inode *, struct file *);
1505 int (*fsync) (struct file *, struct dentry *, int datasync);
1506 int (*aio_fsync) (struct kiocb *, int datasync);
1507 int (*fasync) (int, struct file *, int);
1508 int (*lock) (struct file *, int, struct file_lock *);
1509 ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
1510 unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
1511 int (*check_flags)(int);
1512 int (*flock) (struct file *, int, struct file_lock *);
1513 ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
1514 ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
1515 int (*setlease)(struct file *, long, struct file_lock **);
1516 };
在编写字符设备驱动程序时,常将特定信息放在cdev之后,形成一个设备结构体。
struct xxx_dev
{
struct cdev cdev;
...
};
最后在file_operations所指定的设备读写函数中,涉及到驱动程序(内核地址空间)和应用程序(用户地址空间)的数据交换,为此,内核提供专用函数来完成数据在应用程序空间和驱动程序空间的交换。
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);
put_user(local,user);
get_user(local,user);
【设备驱动中的并发控制】
现代操作系统有三大特征:中断处理、多任务处理、多处理器(SMP)。
在操作系统中,内核提供并发控制机制,来对公有资源进行保护。
并发与竞争:并发(多进程,多线程)容易导致竞争。
并发控制机制提供的同步机制 用来避免 并发导致的竞争问题:原子变量操作、自旋锁、信号量、完成量。
注意:应用程序也有类似的并发控制机制,本文讲的都是内核的并发控制。
1. 原子变量操作:是一种不可被打断的操作。是其他同步手段的基石。
原子整形变量:atomic_t类型的变量只能通过Linux内核中定义的专用函数来操作。
typedef struct {
volatile int counter;
} atomic_t;
原子整形变量的操作:
#define ATOMIC_INIT(i) { (i) }
static inline int atomic_read(const atomic_t *v);
static inline void atomic_set(atomic_t *v, int i);
static inline void atomic_add(int i, atomic_t *v);
static inline void atomic_sub(int i, atomic_t *v);
//等
原子位操作:根据数据的每一位单独进行操作。不需要专门定义一个类似atomic_t类型的变量,只需要一个普通的变量指针就可以了。
static __always_inline void set_bit(unsigned int nr, volatile unsigned long *addr);
static __always_inline void clear_bit(int nr, volatile unsigned long *addr);
static inline void change_bit(int nr, volatile unsigned long *addr);
static inline int test_and_set_bit(int nr, volatile unsigned long *addr);
static inline int test_and_clear_bit(int nr, volatile unsigned long *addr);
2. 自旋锁:是实现 信号量 和 完成量的基础。
20 typedef struct {
21 raw_spinlock_t raw_lock;
22 #ifdef CONFIG_GENERIC_LOCKBREAK
23 unsigned int break_lock;
24 #endif
25 #ifdef CONFIG_DEBUG_SPINLOCK
26 unsigned int magic, owner_cpu;
27 void *owner;
28 #endif
29 #ifdef CONFIG_DEBUG_LOCK_ALLOC
30 struct lockdep_map dep_map;
31 #endif
32 } spinlock_t;
一个自旋锁必须初始化后才能被使用。
96 # define spin_lock_init(lock) 97 do { 98 static struct lock_class_key __key; 99 100 __spin_lock_init((lock), #lock, &__key); 101 } while (0)
在进入临界区代码前,需要锁定自旋锁:
#define spin_lock(lock) _spin_lock(lock)
当不再使用临界区时,要释放自旋锁:
#define spin_unlock(lock) _spin_unlock(lock)
自旋锁的使用:自旋锁是一种忙等待。是一种短时间的轻量级加锁机制,自旋锁不能递归使用。
spin_lock_t lock;//定义一个自旋锁
spin_lock_init(&lock);//初始化自旋锁
spin_lock(&lock);//锁定自旋锁
临界区
spin_unlock(&lock);//释放自旋锁
3. 信号量:与自旋锁相比,信号量只有 得到信号量的进程或线程才能进入临界区,执行临界代码。 当获取的信号量还没被释放时,进程或线程会将自身加入一个等待队列中去睡眠,直到拥有信号量的进程或线程释放信号量后,处于等待队列中的那些进程才会被唤醒,然后被唤醒的进程或线程重新从睡眠的地方开始执行,再一次尝试获得信号量,当获得信号量后,程序继续执行。由于中断需要立刻完成,所以不能睡眠,所以中断程序中不能使用信号量。
16 struct semaphore {
17 spinlock_t lock;
18 unsigned int count;
19 struct list_head wait_list;
20 };
信号量的使用:信号量的使用方法与自旋锁的使用方法基本一样。信号量用来在多个进程或线程之间互斥,它可能引起进程睡眠,睡眠需要进程的上下文切换,耗时,所以相对长时间的被保护临界资源访问时是很好的选择。
首先定义和初始化自旋锁:
struct semaphore sema;//定义一个信号量
static inline void sema_init(struct semaphore *sem, int val);//初始化信号量
#define init_MUTEX(sem) sema_init(sem, 1) //初始化一个互斥信号量
然后锁定信号量:
extern void down(struct semaphore *sem);//调用后导致睡眠 extern int __must_check down_interruptible(struct semaphore *sem);//调用后睡眠,但可被唤醒
最后,当不再使用临界区时,释放信号量:
extern void up(struct semaphore *sem);
4. 完成量:Linux中专门实现一个线程发送一个信号去通知另一个线程开始完成某个任务的机制,叫做完成量。
25 struct completion { 26 unsigned int done; 27 wait_queue_head_t wait; 28 };完成量的使用:
首先定义和初始化完成量:
struct completion com;//定义一个完成量 static inline void init_completion(struct completion *x)//初始化完成量 { x->done = 0; init_waitqueue_head(&x->wait); } //同上 #define DECLARE_COMPLETION(work) struct completion work = COMPLETION_INITIALIZER(work) #efine COMPLETION_INITIALIZER(work) { 0, __WAIT_QUEUE_HEAD_INITIALIZER((work).wait) }
然后,当要实现同步时,调用 等待完成量函数 ,该函数会执行一个不会被信号中断的等待,直到另一个线程完成这个完成量时,此线程才会继续从等待处继续执行下去。
extern void wait_for_completion(struct completion *);
最后,当需要完成同步的任务时,使用下面两个函数均可唤醒完成量。然后 另一个线程中的等待完成量函数之后的代码才被执行。
extern void complete(struct completion *);
extern void complete_all(struct completion *);
【设备驱动中的阻塞和同步机制】
阻塞和非阻塞是设备访问的两种基本方式。在Linux驱动程序中,阻塞进程可以使用等待队列来实现。
等待队列通常与进程调度机制紧密结合,实现内核中的异步事件通知机制。
struct __wait_queue { 33 unsigned int flags; 34 #define WQ_FLAG_EXCLUSIVE 0x01 35 void *private; 36 wait_queue_func_t func; 37 struct list_head task_list; 38 }; typedef struct __wait_queue wait_queue_t; 50 struct __wait_queue_head { 51 spinlock_t lock; 52 struct list_head task_list; 53 }; 54 typedef struct __wait_queue_head wait_queue_head_t;
等待队列的使用:
首先,定义和初始化等待队列头:
struct wait_queue_head_t q;//定义一个等待队列头 #define __WAIT_QUEUE_HEAD_INITIALIZER(name) { .lock = __SPIN_LOCK_UNLOCKED(name.lock), .task_list = { &(name).task_list, &(name).task_list } } #define DECLARE_WAIT_QUEUE_HEAD(name) wait_queue_head_t name = __WAIT_QUEUE_HEAD_INITIALIZER(name)
然后,定义一个等待队列:
62 #define __WAITQUEUE_INITIALIZER(name, tsk) { 63 .private = tsk, 64 .func = default_wake_function, 65 .task_list = { NULL, NULL } } 66 67 #define DECLARE_WAITQUEUE(name, tsk) 68 wait_queue_t name = __WAITQUEUE_INITIALIZER(name, tsk)
其次,用以下两个函数来 从等待队列头q所指向的等待队列链表中 添加或移除 等待队列。
extern void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
extern void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
然后,使用以下宏等待相应的事件:
wait_event(wq, condition) wait_event_timeout(wq, condition, timeout) wait_event_interruptible(wq, condition) wait_event_interruptible_timeout(wq, condition, timeout) //其中:wq是 wait_queue_head_t 类型
最后,使用以下宏来唤醒相应队列中的进程:
#define wake_up(x) __wake_up(x, TASK_NORMAL, 1, NULL) #define wake_up_interruptible(x) __wake_up(x, TASK_INTERRUPTIBLE, 1, NULL) //其中,x是 wait_queue_head_t* 类型
【中断与时钟机制】
1. 中断在Linux中是通过信号来实现的。通常情况下:一个驱动函数只要 申请中断号,并添加中断处理函数就可以了。其他由内核负责完成。
中断分类:硬件中断(可屏蔽,不可屏蔽)、软件中断(异常,系统调用)。
中断的使用:
首先:申请中断(安装中断)
static inline int __must_check request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags, const char *name, void *dev_id) //@irq: 要申请的中断号 //@handler: 要注册的中断处理函数指针 //@name:设备的名字最后,当设备不需要中断时,释放中断
extern void free_irq(unsigned int irq, void *dev_id);
2. 时钟机制:Linux内核中有一个重要的全局变量是HZ。它表示与时钟中断相关的一个值。
#define HZ 1000
Linux内核中的时间延时技术:
短时延时:
static inline void ndelay(unsigned long x);//纳秒级 #define udelay(n) __udelay(n)//微秒级 extern void __udelay(unsigned long usecs); void msleep(unsigned int msecs);//毫秒级 106 #define time_after(a,b) 107 (typecheck(unsigned long, a) && 108 typecheck(unsigned long, b) && 109 ((long)(b) - (long)(a) < 0)) 110 #define time_before(a,b) time_after(b,a)
【内外存的访问】
在Linux中,为了方便编写驱动程序,对内存空间和IO空间的访问提供了一套统一的方法:
申请资源->映射内存空间->访问内存->取消映射->释放资源
kmalloc函数在物理内存中为程序分配一个连续的存储空间。kmalloc函数最小能够分配32字节的内存。
static __always_inline void *kmalloc(size_t size, gfp_t flags) void kfree(const void *objp)vmalloc函数用来分配虚拟地址连续但物理地址不连续的内存。适合用来申请大量的内存。
void *vmalloc(unsigned long size) void vfree(const void *addr)
在内存分配的大多数函数中都涉及物理地址和虚拟地址之间的转换:
static inline unsigned long virt_to_phys(volatile void *address) { return __pa((unsigned long)address); } #define __pa(x) ((unsigned long) (x) - PAGE_OFFSET) static inline void *phys_to_virt(unsigned long address) { return __va(address); } #define __va(x) ((void *)((unsigned long)(x) + PAGE_OFFSET))
设备寄存器可能使用内存空间,也可能使用I/O空间。I/O内存的访问:申请I/O内存,转换I/O内存的物理地址到一个虚拟地址,I/O内存的读写,取消物理地址到虚拟地址的映射。
申请I/O内存:
#define request_mem_region(start,n,name) __request_region(&iomem_resource, (start), (n), (name), 0)
在模块卸载函数中,如不再使用内存资源,使用以下宏释放内存资源
#define release_region(start,n) __release_region(&ioport_resource, (start), (n))
转换I/O内存的物理地址到一个虚拟地址:
static inline void __iomem *ioremap(phys_addr_t offset, unsigned long size)
{
return (void __iomem*) (unsigned long)offset;
}
I/O内存的读写:
unsigned int ioread8(void __iomem *addr) unsigned int ioread16(void __iomem *addr) unsigned int ioread32(void __iomem *addr) void iowrite8(u8 val, void __iomem *addr) void iowrite16(u16 val, void __iomem *addr) void iowrite32(u32 val, void __iomem *addr)
取消物理地址到虚拟地址的映射:
static inline void iounmap(void *addr)
对于使用I/O地址空间的外部设备,需要通过I/O端口和设备传输数据。因此需要向内核申请I/O端口资源。
申请和释放I/O端口:
#define request_region(start,n,name) __request_region(&ioport_resource, (start), (n), (name), 0) #define release_region(start,n) __release_region(&ioport_resource, (start), n))读写I/O端口:
static inline u8 inb(unsigned long addr) static inline u16 inw(unsigned long addr) static inline void outb(u8 b, unsigned long addr) static inline void outw(u16 b, unsigned long addr)
郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。