Linux设备驱动编程第三版-笔记

1章 设备驱动简介

1.1 驱动程序的角色

机制:提供什么能力.

策略:如何使用这些能力.

1.2. 划分内核

    内核的角色可以划分:
    一:进程管理

 

    二:内存管理

    三:文件系统

    四:设备控制

    五:网络

    1.2.1. 可加载模块

Linux设备驱动编程第三版-笔记_第1张图片

1.3. 设备和模块的分类

字符设备:

块设备:

网络接口

 

第 2 章 建立和运行模块

2.2. Hello World 模块

1:使用makfile建立内核树

2:第四章介绍内核的消息的传递的机制,消息保存在哪里的问题.

 

 

2.3. 内核模块相比于应用程序

Linux设备驱动编程第三版-笔记_第2张图片

应用程序:

1:从头到尾执行单个任务.

2:使用库函数,在链接时能解析外部引用.

3:运行在用户空间

内核模块:

1:模块初始化函数的任务是为以后调用模块的函数做准备,

2:仅能调用内核导出的函数

3:内核空间.

避免名字空间污染(namespace pollution),所有符号采用静态变量(static)见"内核符号表".

2.3.1. 用户空间和内核空间

2.3.2. 内核的并发

2.3.3. 当前进程

current 指针指向当前在运行的进程

 

2.4. 编译和加载

2.4.1. 编译模块

请参考GNU make

file:E:\26.2012linux%C5%E0%D1%B5\Linux%20Basic\1.1Linux%20Base%20Introduction\makefile\GNU%20MAKE%20%D6%D0%CE%C4%CA%D6%B2%E1.pdf

2.4.2. 加载和卸载模块

insmod,

lsmod,等价cat /proc/modules

rmmod,

modprobe,

cat /proc/iomem

cat /proc/ioports

2.4.3. 版本依赖

2.4.4. 平台依赖性

 

2.5. 内核符号表

cat /proc/kallsyms 查看内核符号表.

 

2.7. 初始化和关停

1:初始化

static int __init initialization_function(void)

{

 /* Initialization code here */

}

module_init(initialization_function);

2:清理函数

static void __exit cleanup_function(void)

{

 /* Cleanup code here */

}

module_exit(cleanup_function);

2.8. 模块参数

static char *whom = "world";

static int howmany = 1;

module_param(howmany, int, S_IRUGO);

module_param(whom, charp, S_IRUGO);

 

2.10. 快速参考

/sys/module

/proc/modules

/sys/module 是一个 sysfs 目录层次, 包含当前加载模块的信息. /proc/moudles 是旧式的, 那种信息的单个文件版本. 其中的条目包含了模块名, 每个模块占用的内存数量, 以及使用计数. 另外的字串追加到每行的末尾来指定标志, 对这个模块当前是活动的.

eg:

/ # cat /proc/modules

ntfs 98900 0 - Live 0xbf932000

v4l2_common 4980 0 - Live 0xbf91b000

uvcvideo 62464 0 - Live 0xbf908000

videodev 77112 2 v4l2_common,uvcvideo, Live 0xbf8f2000

v4l2_int_device 3520 0 - Live 0xbf8f0000

fuse 69080 0 - Live 0xbf8dc000

mali 233956 6 - Live 0xbf89b000

ump 46392 7 mali, Live 0xbf88e000

blcr 104716 0 - Live 0xbf871000

 

 

第 3 章 字符驱动

3.1. scull 的设计

scull0-scull3:

scullpipe0 -scullpipe3:

scullsingle

scullpriv

sculluid

scullwuid

3.2. 主次编号

主编号标识设备相连的驱动:

次编号被内核用来决定引用哪个设备.

3.2.1. 设备编号的内部表示

3.2.2. 分配和释放设备编号

3.2.3. 主编号的动态分配

 alloc_chrdev_region

3.3. 一些重要数据结构

3.3.1. 文件操作

Linux设备驱动编程第三版-笔记_第3张图片

struct module *owner

第一个 file_operations 成员根本不是一个操作; 它是一个指向拥有这个结构的模块的指针. 这个成员用来在它的操作还在被使用时阻止模块被卸载. 几乎所有时间中, 它被简单初始化为 THIS_MODULE, 一个在 中定义的宏.

 

loff_t (*llseek) (struct file *, loff_t, int);

llseek 方法用作改变文件中的当前读/写位置, 并且新位置作为(正的)返回值. loff_t 参数是一个"long offset", 并且就算在 32位平台上也至少 64 位宽. 错误由一个负返回值指示. 如果这个函数指针是 NULL, seek 调用会以潜在地无法预知的方式修改 file 结构中的位置计数器( 在"file 结构" 一节中描述).

 

ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

用来从设备中获取数据. 在这个位置的一个空指针导致 read 系统调用以 -EINVAL("Invalid argument") 失败. 一个非负返回值代表了成功读取的字节数( 返回值是一个 "signed size" 类型, 常常是目标平台本地的整数类型).

 

ssize_t (*aio_read)(struct kiocb *, char __user *, size_t, loff_t);

初始化一个异步读 -- 可能在函数返回前不结束的读操作. 如果这个方法是 NULL, 所有的操作会由 read 代替进行(同步地).

 

ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

发送数据给设备. 如果 NULL, -EINVAL 返回给调用 write 系统调用的程序. 如果非负, 返回值代表成功写的字节数.

 

ssize_t (*aio_write)(struct kiocb *, const char __user *, size_t, loff_t *);

初始化设备上的一个异步写.

 

int (*readdir) (struct file *, void *, filldir_t);

对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.

 

unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll 方法是 3 个系统调用的后端: poll, epoll, 和 select, 都用作查询对一个或多个文件描述符的读或写是否会阻塞. poll 方法应当返回一个位掩码指示是否非阻塞的读或写是可能的, 并且, 可能地, 提供给内核信息用来使调用进程睡眠直到 I/O 变为可能. 如果一个驱动的 poll 方法为 NULL, 设备假定为不阻塞地可读可写.

 

int (*ioctl) (struct inode *, struct file *, unsigned int, unsigned long);

ioctl 系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道, 这不是读也不是写). 另外, 几个 ioctl 命令被内核识别而不必引用 fops 表. 如果设备不提供 ioctl 方法, 对于任何未事先定义的请求(-ENOTTY, "设备无这样的 ioctl"), 系统调用返回一个错误.

 

int (*mmap) (struct file *, struct vm_area_struct *);

mmap 用来请求将设备内存映射到进程的地址空间. 如果这个方法是 NULL, mmap 系统调用返回 -ENODEV.

 

int (*open) (struct inode *, struct file *);

尽管这常常是对设备文件进行的第一个操作, 不要求驱动声明一个对应的方法. 如果这个项是 NULL, 设备打开一直成功, 但是你的驱动不会得到通知.

 

int (*flush) (struct file *);

flush 操作在进程关闭它的设备文件描述符的拷贝时调用; 它应当执行(并且等待)设备的任何未完成的操作. 这个必须不要和用户查询请求的 fsync 操作混淆了. 当前, flush 在很少驱动中使用; SCSI 磁带驱动使用它, 例如, 为确保所有写的数据在设备关闭前写到磁带上. 如果 flush 为 NULL, 内核简单地忽略用户应用程序的请求.

 

int (*release) (struct inode *, struct file *);

在文件结构被释放时引用这个操作. 如同 open, release 可以为 NULL.

 

int (*fsync) (struct file *, struct dentry *, int);

这个方法是 fsync 系统调用的后端, 用户调用来刷新任何挂着的数据. 如果这个指针是 NULL, 系统调用返回 -EINVAL.

 

int (*aio_fsync)(struct kiocb *, int);

这是 fsync 方法的异步版本.

 

int (*fasync) (int, struct file *, int);

这个操作用来通知设备它的 FASYNC 标志的改变. 异步通知是一个高级的主题, 在第 6 章中描述. 这个成员可以是NULL 如果驱动不支持异步通知.

 

int (*lock) (struct file *, int, struct file_lock *);

lock 方法用来实现文件加锁; 加锁对常规文件是必不可少的特性, 但是设备驱动几乎从不实现它.

 

ssize_t (*readv) (struct file *, const struct iovec *, unsigned long, loff_t *);

ssize_t (*writev) (struct file *, const struct iovec *, unsigned long, loff_t *);

这些方法实现发散/汇聚读和写操作. 应用程序偶尔需要做一个包含多个内存区的单个读或写操作; 这些系统调用允许它们这样做而不必对数据进行额外拷贝. 如果这些函数指针为 NULL, read 和 write 方法被调用( 可能多于一次 ).

 

ssize_t (*sendfile)(struct file *, loff_t *, size_t, read_actor_t, void *);

这个方法实现 sendfile 系统调用的读, 使用最少的拷贝从一个文件描述符搬移数据到另一个. 例如, 它被一个需要发送文件内容到一个网络连接的 web 服务器使用. 设备驱动常常使 sendfile 为 NULL.

 

ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);

sendpage 是 sendfile 的另一半; 它由内核调用来发送数据, 一次一页, 到对应的文件. 设备驱动实际上不实现 sendpage.

 

unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);

这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中. 这个任务通常由内存管理代码进行; 这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求. 大部分驱动可以置这个方法为 NULL.[10]

 

int (*check_flags)(int)

这个方法允许模块检查传递给 fnctl(F_SETFL...) 调用的标志.

 

int (*dir_notify)(struct file *, unsigned long);

这个方法在应用程序使用 fcntl 来请求目录改变通知时调用. 只对文件系统有用; 驱动不需要实现 dir_notify

 

3.3.2. 文件结构

Linux设备驱动编程第三版-笔记_第4张图片

mode_t f_mode;

文件模式确定文件是可读的或者是可写的(或者都是), 通过位 FMODE_READ 和 FMODE_WRITE. 你可能想在你的 open 或者 ioctl 函数中检查这个成员的读写许可, 但是你不需要检查读写许可, 因为内核在调用你的方法之前检查. 当文件还没有为那种存取而打开时读或写的企图被拒绝, 驱动甚至不知道这个情况.

 

loff_t f_pos;

当前读写位置. loff_t 在所有平台都是 64 位( 在 gcc 术语里是 long long ). 驱动可以读这个值, 如果它需要知道文件中的当前位置, 但是正常地不应该改变它; 读和写应当使用它们作为最后参数而收到的指针来更新一个位置, 代替直接作用于 filp->f_pos. 这个规则的一个例外是在 llseek 方法中, 它的目的就是改变文件位置.

 

unsigned int f_flags;

这些是文件标志, 例如 O_RDONLY, O_NONBLOCK, 和 O_SYNC. 驱动应当检查 O_NONBLOCK 标志来看是否是请求非阻塞操作( 我们在第一章的"阻塞和非阻塞操作"一节中讨论非阻塞 I/O ); 其他标志很少使用. 特别地, 应当检查读/写许可, 使用 f_mode 而不是 f_flags. 所有的标志在头文件 中定义.

 

struct file_operations *f_op;

和文件关联的操作. 内核安排指针作为它的 open 实现的一部分, 接着读取它当它需要分派任何的操作时. filp->f_op 中的值从不由内核保存为后面的引用; 这意味着你可改变你的文件关联的文件操作, 在你返回调用者之后新方法会起作用. 例如, 关联到主编号 1 (/dev/null, /dev/zero, 等等)的 open 代码根据打开的次编号来替代 filp->f_op 中的操作. 这个做法允许实现几种行为, 在同一个主编号下而不必在每个系统调用中引入开销. 替换文件操作的能力是面向对象编程的"方法重载"的内核对等体.

 

void *private_data;

open 系统调用设置这个指针为 NULL, 在为驱动调用 open 方法之前. 你可自由使用这个成员或者忽略它; 你可以使用这个成员来指向分配的数据, 但是接着你必须记住在内核销毁文件结构之前, 在 release 方法中释放那个内存. private_data 是一个有用的资源, 在系统调用间保留状态信息, 我们大部分例子模块都使用它.

 

struct dentry *f_dentry;

关联到文件的目录入口( dentry )结构. 设备驱动编写者正常地不需要关心 dentry 结构, 除了作为 filp->f_dentry->d_inode 存取 inode 结构.

 

3.3.3. inode 结构

Linux设备驱动编程第三版-笔记_第5张图片

dev_t i_rdev;

对于代表设备文件的节点, 这个成员包含实际的设备编号.

 

struct cdev *i_cdev;

struct cdev 是内核的内部结构, 代表字符设备; 这个成员包含一个指针, 指向这个结构, 当节点指的是一个字符设备文件时

 

3.4. 字符设备注册

Linux设备驱动编程第三版-笔记_第6张图片

第一步:注册:register_chrdev_region(dev_t,unsigned,const char*)//传入dev_t

第二步:初始化:void cdev_init(struct cdev *cdev, struct file_operations *fops);//得到struct cdev 结构指针

第三步:激活:int cdev_add(struct cdev *dev, dev_t num, unsigned int count);

第四步:销毁:void cdev_del(struct cdev *dev);

3.5. open 和 release

3.5.1. open 方法

int (*open)(struct inode *inode, struct file *filp);

open 方法提供给驱动来做任何的初始化来准备后续的操作. 在大部分驱动中, open 应当进行下面的工作:

 1.检查设备特定的错误(例如设备没准备好, 或者类似的硬件错误

 2.如果它第一次打开, 初始化设备

 3.如果需要, 更新 f_op 指针.

 4.分配并填充要放进 filp->private_data 的任何数据结构

3.5.2. release 方法

 1.释放 open 分配在 filp->private_data 中的任何东西

 2.在最后的 close 关闭设备

 

3.6. scull 的内存使用

在 scull, 每个设备是一个指针链表, 每个都指向一个 scull_dev 结构. 每个这样的结构, 缺省地, 指向最多 4 兆字节, 通过一个中间指针数组. 发行代码使用一个 1000 个指针的数组指向每个 4000 字节的区域. 我们称每个内存区域为一个量子, 数组(或者它的长度) 为一个量子集. 一个 scull 设备和它的内存区如图一个 scull 设备的布局所示.

Linux设备驱动编程第三版-笔记_第7张图片

3.7. 读和写

ssize_t read(struct file *filp, char __user *buff, size_t count, loff_t *offp);

ssize_t write(struct file *filp, const char __user *buff, size_t count, loff_t *offp);

3.7.1. read 方法

1.如果这个值等于传递给 read 系统调用的 count 参数, 请求的字节数已经被传送. 这是最好的情况.

2.如果是正数, 但是小于 count, 只有部分数据被传送. 这可能由于几个原因, 依赖于设备. 常常, 应用程序重新试着读取. 例如, 如果你使用 fread 函数来读取, 库函数重新发出系统调用直到请求的数据传送完成.

3.如果值为 0, 到达了文件末尾(没有读取数据).

4.一个负值表示有一个错误. 这个值指出了什么错误, 根据 . 出错的典型返回值包括 -EINTR( 被打断的系统调用) 或者 -EFAULT( 坏地址 ).

3.7.2. write 方法

1.如果值等于 count, 要求的字节数已被传送.

2.如果正值, 但是小于 count, 只有部分数据被传送. 程序最可能重试写入剩下的数据.

3.如果值为 0, 什么没有写. 这个结果不是一个错误, 没有理由返回一个错误码. 再一次, 标准库重试写调用. 我们将在第 6 章查看这种情况的确切含义, 那里介绍了阻塞.

4.一个负值表示发生一个错误; 如同对于读, 有效的错误值是定义于 中.

3.7.3. readv 和 writev

 

 

第 4 章 调试技术

 

4.2. 用打印调试

4.2.1. printk

1.记录级别或者优先级在消息上Linux设备驱动编程第三版-笔记_第8张图片

2.也可以通过文本文件 /proc/sys/kernel/printk 读写控制台记录级别.

 # echo 8 > /proc/sys/kernel/printk

4.2.2. 重定向控制台消息

4.2.3. 消息是如何记录的

4.2.4. 打开和关闭消息

4.2.5. 速率限制

4.2.6. 打印设备编号

 

4.3. 用查询来调试(cat /proc/****)

4.3.1. 使用 /proc 文件系统

 

4.4. 使用观察来调试

运行命令 strace

 

4.5. 调试系统故障

4.5.1. oops 消息

4.5.2. 系统挂起
4.6. 调试器和相关工具

 

 

第 5 章 并发和竞争情况

5.1. scull中的缺陷

2个进程同时访问一个函数:即由并发引起的竞争.

 

5.2. 并发和它的管理

矛盾:硬件/软件资源需要共享,共享就存在并发/竞争.

即:当 2 个执行的线路[17]有机会操作同一个数据结构(或者硬件资源), 混合的可能性就一直存在

解决办法:

加锁.数据结构的操作原子化

 

5.3. 旗标和互斥体

进程在等待锁,去睡眠/阻塞.(但是有几种情况不能使用.)

旗标:在计算机科学中是一个被很好理解的概念. 在它的核心, 一个旗标是一个单个整型值, 结合有一对函数, 典型地称为 P(加锁) 和 V(解锁). 一个想进入临界区的进程将在相关旗标上调用 P; 如果旗标的值大于零, 这个值递减 1 并且进程继续. 相反, 如果旗标的值是 0 ( 或更小 ), 进程必须等待直到别人释放旗标. 解锁一个旗标通过调用 V 完成; 这个函数递增旗标的值, 并且, 如果需要, 唤醒等待的进程.

5.3.1. Linux 旗标实现

linux/semapore.h

Linux设备驱动编程第三版-笔记_第9张图片

1);创建:

void sema_init(struct semaphore *sem, int val);

这里 val 是安排给旗标的初始值.(通常有0/1)

然而, 通常旗标以互斥锁的模式使用. 为使这个通用的例子更容易些, 内核提供了一套帮助函数和宏定义. 因此, 一个互斥锁可以声明和初始化, 使用下面的一种:

DECLARE_MUTEX(name);

DECLARE_MUTEX_LOCKED(name);//注释

这里, 结果是一个旗标变量( 称为 name ), 初始化为 1 ( 使用 DECLARE_MUTEX ) 或者 0 (使用 DECLARE_MUTEX_LOCKED ). 在后一种情况, 互斥锁开始于上锁的状态; 在允许任何线程存取之前将不得不显式解锁它.

如果互斥锁必须在运行时间初始化( 这是如果动态分配它的情况, 举例来说), 使用下列中的一个:

void init_MUTEX(struct semaphore *sem);

void init_MUTEX_LOCKED(struct semaphore *sem);

2) 加锁/P 函数(系列down函数):这个函数递减旗标的值,
void down(struct semaphore *sem);

//注释:down 递减旗标值并且等待需要的时间

int down_interruptible(struct semaphore *sem);

//但是操作是可中断的. 这个可中断的版本几乎一直是你要的那个; 它允许一个在等待一个旗标的用户空间进程被用户中断. 作为一个通用的规则, 你不想使用不可中断的操作, 除非实在是没有选择. 不可中断操作是一个创建不可杀死的进程( 在 ps 中见到的可怕的 "D 状态" )和惹恼你的用户的好方法, 使用 down_interruptible 需要一些格外的小心, 但是, 如果操作是可中断的, 函数返回一个非零值, 并且调用者不持有旗标. 正确的使用 down_interruptible 需要一直检查返回值并且针对性地响应.

int down_trylock(struct semaphore *sem);

//从不睡眠; 如果旗标在调用时不可用, down_trylock 立刻返回一个非零值

3)解锁/U 函数:

void up(struct semaphore *sem);

一旦 up 被调用, 调用者就不再拥有旗标.

 

5.3.2. 在 scull 中使用旗标

 

5.3.3. 读者/写者旗标
 .

1);创建:

void init_rwsem(struct rw_semaphore *sem);

2):读接口

void down_read(struct rw_semaphore *sem);

int down_read_trylock(struct rw_semaphore *sem);

void up_read(struct rw_semaphore *sem);

对 down_read 的调用提供了对被保护资源的只读存取, 与其他读者可能地并发地存取. 注意 down_read 可能将调用进程置为不可中断的睡眠. down_read_trylock 如果读存取是不可用时不会等待; 如果被准予存取它返回非零, 否则是 0. 注意 down_read_trylock 的惯例不同于大部分的内核函数, 返回值 0 指示成功. 一个使用 down_read 获取的 rwsem 必须最终使用 up_read 释放.

3):写接口

读者的接口类似:

void down_write(struct rw_semaphore *sem);

int down_write_trylock(struct rw_semaphore *sem);

void up_write(struct rw_semaphore *sem);

void downgrade_write(struct rw_semaphore *sem);

down_write, down_write_trylock, 和 up_write 全部就像它们的读者对应部分, 除了, 当然, 它们提供写存取. 如果你处于这样的情况, 需要一个写者锁来做一个快速改变, 接着一个长时间的只读存取, 你可以使用 downgrade_write 在一旦你已完成改变后允许其他读者进入.

 

5.4. Completions 机制

1 问题:在进程之间传递事件完成,效率不高.

内核编程的一个普通模式包括在当前线程之外初始化某个动作, 接着等待这个动作结束. 这个动作可能是创建一个新内核线程或者用户空间进程, 对一个存在着的进程的请求, 或者一些基于硬件的动作. 在这些情况中, 很有诱惑去使用一个旗标来同步 2 个任务, 使用这样的代码:

struct semaphore sem;

init_MUTEX_LOCKED(&sem);

start_external_task(&sem);

down(&sem);

外部任务可以接着调用 up(??sem), 在它的工作完成时.

事实证明, 这种情况旗标不是最好的工具. 正常使用中, 试图加锁一个旗标的代码发现旗标几乎在所有时间都可用; 如果对旗标有很多竞争, 性能会受损并且加锁方案需要重新审视. 因此旗标已经对"可用"情况做了很多的优化. 当用上面展示的方法来通知任务完成, 然而, 调用 down 的线程将几乎是一直不得不等待; 因此性能将受损. 旗标还可能易于处于一个( 困难的 ) 竞争情况, 如果它们表明为自动变量以这种方式使用时. 在一些情况中, 旗标可能在调用 up 的进程用完它之前消失.

2 解决办法:/"completion" 接口

 

1) 必须动态创建和初始化

init_completion(&my_completion);

2) 等待 completion

void wait_for_completion(struct completion *c);

3)真正的 completion 事件

void complete(struct completion *c);

void complete_all(struct completion *c);

 

5.5. 自旋锁

概念:

自旋锁概念上简单. 一个自旋锁是一个互斥设备, 只能有 2 个值:"上锁"和"解锁". 它常常实现为一个整数值中的一个单个位. 想获取一个特殊锁的代码测试相关的位. 如果锁是可用的, 这个"上锁"位被置位并且代码继续进入临界区. 相反, 如果这个锁已经被别人获得, 代码进入一个紧凑的循环中反复检查这个锁, 直到它变为可用. 这个循环就是自旋锁的"自旋"部分

5.5.1. 自旋锁 API 简介

自旋锁原语要求的包含文件是 . 一个实际的锁有类型 spinlock_t. 象任何其他数据结构, 一个 自旋锁必须初始化. 这个初始化可以在编译时完成, 如下:

spinlock_t my_lock = SPIN_LOCK_UNLOCKED;

或者在运行时使用:

void spin_lock_init(spinlock_t *lock);

 

在进入一个临界区前, 你的代码必须获得需要的 lock , 用:

void spin_lock(spinlock_t *lock);

注意所有的自旋锁等待是, 由于它们的特性, 不可中断的. 一旦你调用 spin_lock, 你将自旋直到锁变为可用.

 

为释放一个你已获得的锁, 传递它给:

void spin_unlock(spinlock_t *lock);

有很多其他的自旋锁函数, 我们将很快都看到. 但是没有一个背离上面列出的函数所展示的核心概念. 除了加锁和释放, 没有什么可对一个锁所作的. 但是, 有几个规则关于你必须如何使用自旋锁. 我们将用一点时间来看这些, 在进入完整的自旋锁接口之前.

5.5.2. 自旋锁和原子上下文

问题:死锁:自旋锁中的操控是原子操作,避免进程去睡眠,失去处理器,如数据copy:copy_from_user,kmalloc==.

第一种情况(内核方面):

想象一会儿你的驱动请求一个自旋锁并且在它的临界区里做它的事情. 在中间某处, 你的驱动失去了处理器. 或许它已调用了一个函数( copy_from_user, 假设) 使进程进入睡眠. 或者, 也许, 内核抢占发威, 一个更高优先级的进程将你的代码推到一边. 你的代码现在持有一个锁, 在可见的将来的如何时间不会释放这个锁. 如果某个别的线程想获得同一个锁, 它会, 在最好的情况下, 等待( 在处理器中自旋 )很长时间. 最坏的情况, 系统可能完全死锁.

第二种情况(设备方面):

你的驱动在执行并且已经获取了一个锁来控制对它的设备的存取. 当持有这个锁时, 设备发出一个中断, 使得你的中断处理运行. 中断处理, 在存取设备之前, 必须获得锁. 在一个中断处理中获取一个自旋锁是一个要做的合法的事情; 这是自旋锁操作不能睡眠的其中一个理由. 但是如果中断处理和起初获得锁的代码在同一个处理器上会发生什么? 当中断处理在自旋, 非中断代码不能运行来释放锁. 这个处理器将永远自旋.

 

避免这个陷阱需要在持有自旋锁时禁止中断( 只在本地 CPU ). 有各种自旋锁函数会为你禁止中断( 我们将在下一节见到它们 ). 但是, 一个完整的中断讨论必须等到第 10 章了

 

5.5.3. 自旋锁函数

1)加锁函数:

void spin_lock(spinlock_t *lock);

void spin_lock_irqsave(spinlock_t *lock, unsigned long flags);禁止中断,在获得自旋锁之前; 之前的中断状态保存在 flags 里.

void spin_lock_irq(spinlock_t *lock);//

void spin_lock_bh(spinlock_t *lock);//在获取锁之前禁止软件中断, 但是硬件中断留作打开的.

 

2)解锁函数:

void spin_unlock(spinlock_t *lock);

void spin_unlock_irqrestore(spinlock_t *lock, unsigned long flags);

void spin_unlock_irq(spinlock_t *lock);

void spin_unlock_bh(spinlock_t *lock);

 

3)还有一套非阻塞的自旋锁操作:

int spin_trylock(spinlock_t *lock);

int spin_trylock_bh(spinlock_t *lock);

 

5.5.4. 读者/写者自旋锁

内核提供了一个自旋锁的读者/写者形式, 直接模仿我们在本章前面见到的读者/写者旗标. 这些锁允许任何数目的读者同时进入临界区, 但是写者必须是排他的存取. 读者写者锁有一个类型 rwlock_t, 在 中定义. 它们可以以 2 种方式被声明和被初始化:

 

rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* Static way */

rwlock_t my_rwlock;

rwlock_init(&my_rwlock); /* Dynamic way */

 

可用函数的列表现在应当看来相当类似. 对于读者, 下列函数是可用的:

 

void read_lock(rwlock_t *lock);

void read_lock_irqsave(rwlock_t *lock, unsigned long flags);

void read_lock_irq(rwlock_t *lock);

void read_lock_bh(rwlock_t *lock);

 

void read_unlock(rwlock_t *lock);

void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

void read_unlock_irq(rwlock_t *lock);

void read_unlock_bh(rwlock_t *lock);

 

有趣地, 没有 read_trylock. 对于写存取的函数是类似的:

 

void write_lock(rwlock_t *lock);

void write_lock_irqsave(rwlock_t *lock, unsigned long flags);

void write_lock_irq(rwlock_t *lock);

void write_lock_bh(rwlock_t *lock);

int write_trylock(rwlock_t *lock);

 

void write_unlock(rwlock_t *lock);

void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags);

void write_unlock_irq(rwlock_t *lock);

void write_unlock_bh(rwlock_t *lock);

读者/写者锁能够饿坏读者, 就像 rwsem 一样. 这个行为很少是一个问题; 然而, 如果有足够的锁竞争来引起饥饿, 性能无论如何都不行.

 

5.6. 锁陷阱

1)一个函数需要一个锁,接着调用另外一个函数也试图请求这个锁,就会死锁.

2)外部调用的函数必须明确处理加锁.内部函数/静态函数调用者已经获取了相关的锁,但是call其他函数要明确是否持有锁.

3)尽力避免获取多个锁,如果没法避免,就以同意的顺序去获取锁.

4)如果你确实怀疑锁竞争在损坏性能, 你可能发现 lockmeter 工具有用. 这个补丁(从 http://oss.sgi.com/projects/lockmeter/ 可得到) 装备内核来测量在锁等待花费的时间. 通过看这个报告, 你能够很快知道是否锁竞争真的是问题.

 

5.7. 加锁的各种选择

5.7.1 环形缓冲

 

5.7.2. 原子变量(没有必要为一个共享的全局变量而加锁)

 

1)初始化:

atomic_t v = ATOMIC_INIT(0);

void atomic_set(atomic_t *v, int i);

2)数据操作:

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_inc_and_test(atomic_t *v);

int atomic_dec_and_test(atomic_t *v);

int atomic_sub_and_test(int i, atomic_t *v);

int atomic_add_negative(int i, 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);

 

5.7.3. 位操作

/

2)数据操作:

void set_bit(nr, void *addr);

void clear_bit(nr, void *addr);

void change_bit(nr, void *addr);

test_bit(nr, void *addr);

 

int test_and_set_bit(nr, void *addr);

int test_and_clear_bit(nr, void *addr);

int test_and_change_bit(nr, void *addr);

 

5.7.4. seqlock

seqlock 通常不能用在保护包含指针的数据结构, 因为读者可能跟随着一个无效指针而写者在改变数据结构.

.

1)初始化:

seqlock_t lock1 = SEQLOCK_UNLOCKED;

seqlock_t lock2;

seqlock_init(&lock2);

2) 读操作:

读存取通过在进入临界区入口获取一个(无符号的)整数序列来工作. 在退出时, 那个序列值与当前值比较; 如果不匹配, 读存取必须重试. 结果是, 读者代码象下面的形式:

unsigned int seq;

do {

    seq = read_seqbegin(&the_lock);

    /* Do what you need to do */

} while read_seqretry(&the_lock, seq);

IRQ 安全的版本

unsigned int read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags);

int read_seqretry_irqrestore(seqlock_t *lock, unsigned int seq, unsigned long flags);

 

3) 写操作

void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags);

void write_seqlock_irq(seqlock_t *lock);

void write_seqlock_bh(seqlock_t *lock);

void write_sequnlock_irqrestore(seqlock_t *lock, unsigned long flags);

void write_sequnlock_irq(seqlock_t *lock);

void write_sequnlock_bh(seqlock_t *lock);

 

5.7.5. 读取-拷贝-更新

使用 RCU 的代码应当包含 .

struct my_stuff *stuff;

rcu_read_lock();

stuff = find_the_stuff(args...);

do_something_with(stuff);

rcu_read_unlock();

 

 

第 6 章 高级字符驱动操作

Linux设备驱动编程第三版-笔记_第10张图片

6.1. ioctl 接口

1)在用户空间, ioctl 系统调用有下面的原型:

int ioctl(int fd, unsigned long cmd, ...);

特点:ioctl 调用的非结构化,在不同的系统中控制操作混杂,主要通过包括嵌入命令到数据流/使用虚拟文件系统(要么是 sysfs 要么是设备特定的文件系统.)

2)内核态原型

int (*ioctl) (struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg);

6.1.1. 选择 ioctl 命令

Linux设备驱动编程第三版-笔记_第11张图片

type

魔数. 只是选择一个数(在参考了 ioctl-number.txt之后)并且使用它在整个驱动中. 这个成员是 8 位宽(_IOC_TYPEBITS).

number

序(顺序)号. 它是 8 位(_IOC_NRBITS)宽.

direction

数据传送的方向,如果这个特殊的命令涉及数据传送. 可能的值是 _IOC_NONE(没有数据传输), _IOC_READ, _IOC_WRITE, 和 _IOC_READ|_IOC_WRITE (数据在2个方向被传送). 数据传送是从应用程序的观点来看待的; _IOC_READ 意思是从设备读, 因此设备必须写到用户空间. 注意这个成员是一个位掩码, 因此 _IOC_READ 和 _IOC_WRITE 可使用一个逻辑 AND 操作来抽取.

size

涉及到的用户数据的大小. 这个成员的宽度是依赖体系的, 但是常常是 13 或者 14 位. 你可为你的特定体系在宏 _IOC_SIZEBITS 中找到它的值. 你使用这个 size 成员不是强制的 - 内核不检查它 -- 但是它是一个好主意. 正确使用这个成员可帮助检测用户空间程序的错误并使你实现向后兼容, 如果你曾需要改变相关数据项的大小. 如果你需要更大的数据结构, 但是, 你可忽略这个 size 成员. 我们很快见到如何使用这个成员

6.1.2. 返回值

6.1.3. 预定义的命令

预定义命令分为 3 类:

1)可对任何文件发出的(常规, 设备, FIFO, 或者 socket) 的那些.//

如:
FIOCLEX

设置 close-on-exec 标志(File IOctl Close on EXec). 设置这个标志使文件描述符被关闭, 当调用进程执行一个新程序时.

FIONCLEX

清除 close-no-exec 标志(File IOctl Not CLose on EXec). 这个命令恢复普通文件行为, 复原上面 FIOCLEX 所做的. FIOASYNC 为这个文件设置或者复位异步通知(如同在本章中"异步通知"一节中讨论的). 注意直到 Linux 2.2.4 版本的内核不正确地使用这个命令来修改 O_SYNC 标志. 因为两

你可能感兴趣的:(Linux)