[TOC]

驱动编写

注册驱动模块

1
2
3
//fun的参数列表为空,返回值为int
module_init(fun_init);
module_exit(fun_exit);
1
2
3
4
5
6
7
//其它声明
//[必须]开源声明
MODULE_LICENSE("GPL");
//作者
MODULE_AUTHOR("cxcc");
//描述
MODULE_DESCRIPTION("read and write test module");

输出操作

1
printk(输出等级 "fmt",...);

输出等级

1
2
3
4
5
6
7
8
#define KERN_EMERG  KERN_SOH "0"    /* system is unusable */
#define KERN_ALERT KERN_SOH "1" /* action must be taken immediately */
#define KERN_CRIT KERN_SOH "2" /* critical conditions */
#define KERN_ERR KERN_SOH "3" /* error conditions */
#define KERN_WARNING KERN_SOH "4" /* warning conditions */
#define KERN_NOTICE KERN_SOH "5" /* normal but significant condition */
#define KERN_INFO KERN_SOH "6" /* informational */
#define KERN_DEBUG KERN_SOH "7" /* debug-level messages */

如果输出等级高于控制台等级,就会在控制台输出该消息,否则只能在dmesg中查看

cdev操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//将设备添加到系统中
//参数:
// p:要添加到系统中的cdev
// dev:设备号
// count:添加到系统中的设备个数
int cdev_add(struct cdev *p, dev_t dev,unsigned count)
//将设备从系统中删除
//参数:
// p:设备cdev
void cdev_del(struct cdev *p);
//关联cdev与操作接口
//参数:
// cdev:设备信息结构体
// fops:操作接口
void cdev_init(struct cdev *cdev, const struct file_operations *fops)
//调用函数直接返回一个可用cdev首地址
struct cdev *cdev_alloc(void);

创建设备

手动

1
2
3
4
5
#使用mknod 创建设备
#设备类型使用c表示字符设备,使用b表示块设备
sudo mknod [设备路径] [设备类型(c/b)] [主设备号] [次设备号]
#例如:创建字符设备/dev/rw_dev0 设备号为[504:0]
sudo mknod /dev/rw_dev0 c 504 0

自动

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//注册设备class
//该函数为宏函数,下面列出的不是函数声明,仅是以函数声明的形式列出输入和输出的类型
//参数:
// owner:一般使用THIS_MOUDLE
// name:模块名字符串
//返回值:
// 设备class
struct class * class_create(struct module *owner,const char* name);
//注销设备class
//参数:
// cls:设备class
void class_destroy(struct class *cls);


//创建设备文件
//参数:
// class:设备class
// parent:设备的父节点,若没有则置空(NULL)
// devt:设备号
// drvdata:传递参数使用(暂时不会用)
// fmt:设备名,该参数和后续的参数可以像print一样使用,即可以使用占位符进行格式化输出
struct device *device_create(
struct class *class,
struct device *parent,
dev_t devt,
void *drvdata,
const char *fmt, ...)
//删除设备
//参数:
// class:设备class
// devt:设备号
void device_destroy(struct class *class, dev_t devt)

设备号操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//静态申请设备号
//参数:
// from:要申请的设备号
// count:要申请的次设备号的个数(申请出的次设备号连续,起始值是第一个参数中的次设备号)
// name:驱动名(参考其他书写的是设备名称,但是这个名称和实际的设备名称好像并没有什么关系)
// 在/proc/devices目录中可查
//返回值:返回0表示申请成功,否则返回错误码
int register_chrdev_region(dev_t from, unsigned count, const char *name);
//动态申请设备号
//参数:
// dev:[output]要存放设备号的结构体地址
// baseminor:起始设备号
// count:要申请的次设备号的个数(申请出的次设备号连续)
// name:驱动名(参考其他书写的是设备名称,但是这个名称和实际的设备名称好像并没有什么关系)
// 在/proc/devices目录中可查
//返回值:返回0表示申请成功,否则返回错误码
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char* name);
//释放设备号
//参数:
// from:要释放的设备号
// count:子设备号个数
void unregister_chrdev_region(dev_t from, unsigned count);

数据拷贝

1
2
3
4
5
6
7
//__user 标识是用户的缓冲区
//n 拷贝的数据长度

//从用户内存拷贝到内核内存
static inline long copy_from_user(void *to, const void __user * from, unsigned long n);
//从内核内存拷贝到用户内存
static inline long copy_to_user(void __user *to, const void *from, unsigned long n);

符号表导出

1
2
3
//导出一个符号,供其他模块调用
EXPORT_SYMBOL(name)
EXPORT_SYMBOL_GPL(name)

参数传递

1
2
//参数名,类型,参数文件的权限(如果是0则不创建参数文件)()
module_param(name, type, perm)

地址映射

1
2
void __iomem* imremap(phys_addr_t offset,unsigned long size);
void iounmap(void __iomem* addr)

读写

1
2
3
4
5
//b是1bit,是4bit,c是映射后的虚拟地址
readb(c)
writeb(v,c)
readl(c)
writel(v,c)

GPIO子系统

1
2
3
4
5
6
7
8
9
10
11
```



## IRQ子系统

```C
//irq:软件中断号,通过gpio_to_irq获取
//handle:中断处理函数
request_irq(unsigned int irq,irq_handler_t handler,unsigned long flags,const char*name,void *dev)
free_irq

中断底半部

软件中断

​ 处于中断状态,独占CPU,不参与调度,能被IRQ打断。没有内核接口不可用(除非修改内核,导出接口)

1
2
3
4
//注册一个软中断
open_softirq
//触发软中断
raise_softirq

tasklet

​ 基于软中断实现,特性和软中断相同。

1
2
3
4
//初始化
tasklet_init
//调度
tasklet_schedule

工作队列

​ 进程模式,参与进程调度

​ <linux/workqueue.h>

方法一:

1
2
3
4
//初始化work
INIT_WORK(_work,_func)
//调度work
schedule_work()

方法二:

1
2
3
4
5
6
//创建工作队列
create_workqueue()
//销毁工作队列
destory_workqueue()
//添加工作到工作队列
queue_work

内核定时器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct timer_list
{
unsigned long expires;//定时时间 jiffies计数器,系统启动开始计时,计数频率对应的宏:HZ
void(*function)(unsigned long );//处理函数
}
// expires = jiffies + x*HZ (x秒的定时)

//定义定时器
DEFINE_TIMER(_name,_function,expires,_data)
//初始化定时器
init_timer()
//添加定时器到内核
add_timer
//删除定时器
del_timer
//修改定时器定时时间
mod_timer

Kfifo

<linux/kfifo.h>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
struct kfifo{
unsigned char*buffer;
unsigned int size;
unsigned int in;
unsigned int out;
}
/**
* gfp_mask:
* GFP_KERNEL:在分配空间时可以参与内核调度(常用)
* GFP_ATOMIC:原子性操作,在分配空间时不允许被打断
*/
kfifo_alloc(fifo,size,gfp_mask)
kfifo_free()

kfifo_in
kfifo_out

//存储数据的长度
kfifo_len
//fifo大小
kfifo_size
//是否为空
kfifo_is_empty
//是否满
kfifo_is_full

内存申请

1
2
3
4
5
6
7
8
9
10
/**
* gfp_mask:GFP_KERNEL,GFP_ATOMIC
*/
//分配出的内存在物理内存空间和虚拟内存空间上均连续,因此分配的大小不能太大,否则可能在物理空间中找不到,导致失败
kmalloc(size,gfp_mask)
kfree()

//虚拟内存空间上连续,在物理内存空间不一定连续
vmalloc(size)
vfree()

并发竟态

同步:程序执行的顺序性

互斥:程序执行的排他性

  1. 互斥锁

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //<linux/mutex.h>
    //互斥锁初始化
    mutex_init(mutex)
    //上锁(上锁失败,阻塞)
    mutex_lock()
    //尝试上锁(上锁失败返回错误)
    mutex_trylock
    //解锁
    mutex_unlock()
  2. 自旋锁

    上锁成功:停止进程调度器

    上锁失败:进程自旋,不休眠,100%占用CPU(其他进程在临界区内主动释放CPU)

    1
    2
    3
    4
    5
    6
    7
    //<linux/spinlock.h>
    //初始化
    spin_lock_init()
    //
    spin_lock
    spin_trylock
    spin_unlock
  3. 原子操作

    不会被打断的操作(ATOMIC)一般是由汇编编写,方法一般在体系架构相关的代码中

    (arch/<架构>/include)

  4. 信号量

    semaphore

    1
    2
    3
    4
    5
    6
    7
    8
    9
    //初始化
    sema_init
    //减操作,不可被中断
    down
    //加操作
    up
    down_trylock
    //减操作,可被其他系统信号中断,例如Ctrl+C
    down_interruptible

IO模型

阻塞

注意,在等待队列的一般是由于缓冲区无数据导致的读取等待。因此应该在有进程向缓冲区写数据的时候唤醒,或者从硬件读取到数据时唤醒

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//<linux/wait.h>


//wq 等待队列头
//condition 条件(0:条件不满足,1:条件满足)
wait_event(wq,condition)
//可中断的
wait_event_interrupttible(wq,condition)

//初始化等待队列头
init_waitqueue_head()
//初始化等待队列项
//@q:队列
//@task:进程信息,CURRENT
init_waitqueue_entry(q,task)
//添加到等待队列
add_wait_queue()
//切换进程状态(sched.h)
//@state_value:进程状态码,例如:TASK_RUNNING
set_current_state(state_value)
//进程切换
schedule
  1. 初始化等待队列头
  2. 初始化等待队列项
  3. 将等待队列项添加到等待队列上
  4. 切换进程的状态
  5. 调度执行其他进行
1
2
3
4
5
6
//唤醒
wake_up
wake_up_interruptible

//从等待队列移除
remove_wait_queue
  1. 切换进程状态(TASK_RUNNING)
  2. 从等待队列移除 remove_wait_queue

非阻塞

return -EAGAIN;

判断应用层的打开方式是阻塞还是非阻塞

file结构体:f_flags O_NONBLOCK 00004000 非阻塞

应用层打开时,第二个参数中或上O_NONBLOCK

或者使用fcntl F_GETL F_SETL

IO多路复用(select/poll/epoll)

应用层

(153条消息) c语言select函数作用,linux c语言 select函数使用方法_謝晓东的博客-CSDN博客

1
int select()

驱动层实现file_optional->poll

1
2
3
4
/**
* @return (0:条件不满足,返回非0:条件满足,常用POLLIN,POLLOUT)
*/
int (*poll)

异步通知

驱动层通知应用层(采用信号机制)

实现支持异步通知功能,应用层需要开启异步通知,内核中的驱动需要支持异步通知

  1. 写FASYNC标志到fd中,通过fcntl设置;设置F_SETOWN,绑定应用层进程
    1. fcntl(fd,F_SETOWN,getpid())//绑定进程
    2. fcntl(fd,SETFL,fcntl(fd,GETFL)|FASYNC)//开启异步通知
  2. 驱动中支持,file_optional->fasync(int fd,struct file* filep,int on)
    1. 一般会调用,fasync_helper()
    2. 条件满足,发送信号给应用层进程(SIGIO)
      • kill_fasync

文件操作

open

1
2
3
//inode:【输入】描述了上层设备文件节点的结构体
//file:【输出】文件指针
int open(struct inode * in, struct file * fp)

ioctl

1
2
3
4
5
//应用层接口
//fd:文件
//request:请求号
//...:参数
int ioctl(int fd,unsigned long request,...)
1
2
3
4
5
//驱动层接口
//file:文件
//cmd:
long (*unlocked_ioctl) (struct file *, unsigned int cmd, unsigned long args);
long (*compat_ioctl) (struct file *, unsigned int, unsigned long);

cmd的构造方法

1
2
3
4
_IO
_IOW
_IOR
_IOWR

cmd中提取参数

1
2
3
4
_IOC_DIR
_IOC_TYPE
_IOC_NR
_IOC_SIZE
0%