1. Linux文件和常用函数总结
1.1 inode和文件描述符
inode 或
i
节点是指对文件的索引。如一个系统,所有文件是放在磁盘或
flash
上,就要编个目录来说 明每个文件在什么地方,有什么属性,及大小等。就像书本的目录一样,便于查找和管理。
在linux
中,
内核通过
inode
来找到每个文件
,但一个文件可以被许多用户同时打开或一个用户同时 打开多次。这就有一个问题,如何管理文件的当前位移量
,因为可能每个用户打开文件后进行的操作都 不一样,这样文件位移量也不同,当然还有其他的一些问题。所以linux
又搞了一个文件描述符(
file descriptor)这个东西,
来分别为每一个用户服务。每个用户每次打开一个文件,就产生一个文件描述
符,多次打开就产生多个文件描述符,一一对应,不管是同一个用户,还是多个用户。该文件描述符就
记录了当前打开的文件的偏移量等数据
。所以一个
i
节点可以有
0
个或多个文件描述符。多个文件描述符 可以对应一个i
节点。
1.2 gcc -O是什么
使用编译优化级别1编译程序。级别为1~3,级别越大优化效果越好,但编译时间越长。
一个字符设备或者块设备都有一个主设备号和次设备号。主设备号和次设备号统称为设备号。主设备号用来表示一个特定的驱动程序。次设备号用来表示使用该驱动程序的各设备。例如一个嵌入式系 统,有两个LED指示灯,LED灯需要独立的打开或者关闭。那么,可以写一个LED灯的字符设备驱动程 序,可以将其主设备号注册成5号设备,次设备号分别为1和2。这里,次设备号就分别表示两个LED灯。 设备文件通常都在 /dev 目录下。如:
1.3 bin文件和elf文件区别
GCC 编译出来的是
ELF
文件。通常
gcc –o test test.c,
生成的
test
文件就是
ELF
格式的,在
linuxshell
下 输入 ./test
就可以执行。
bin文件是经过压缩的可执行文件,
去掉
ELF
格式的东西
,
是直接的内存映像的表示。
在系统没有加载
操作系统的时候可以执行
。
机器最终只认BIN
,之所以有
ELF
格式是在有操作系统时,操作系统会根据
ELF
解析出代码、数据等
等,最终仍是以
BIN
运行
。
Bin文件是一个没有内存修改或重定位的
纯二进制文件
,很可能它有明确的指令要加载到特定的内存地址。
ELF文件是可执行的可链接格式,它由一个符号查找表和可重定位表组成。也就是说,它可以由内核加载到任何内存地址,并且自动地将所有使用的符号调整为与该内存地址的偏移量被装入。通常,ELF
件有很多部分,比如'data'
,
'text'
,
'bss'
等等。
1.4 介绍下file_operations结构体
1.5 copy_from_user open read write等常用函数总结
2. 常见指令
2.1 Linux常用指令
2.2 GCC常用指令
2.3 GDB调试命令(需要学习GDB调试)
显示当前代码:l(显示10行)
在某一行打断点:
b
行号
开始运行程序:
r( run )
逐步执行:
s( step ) ,
类似于
VS
中的
F11
重复上条命令:不输入任何命令,直接
enter
查看某个变量的变化:
print
变量名
查看变量的类型:
whatis
变量名
运行至当前函数结束:
finish
退出调试:
q
2.4 内核中空间的分配函数
申请小于128kb字节的连续物理内存
分配线性连续物理不连续的大内存(1GB以下)
DMA是一种硬件机制,允许外围设备和主存之间直接传输
IO
数据,而不需要
CPU
的参与,使用 DMA机制能大幅提高与设备通信的吞吐量。
ioremap是一种更直接的内存
“
分配
”
方式,使用时直接指定物理起始地址和需要分配内存的大小, 然后将该段物理地址映射到内核地址空间。
3. Linux驱动开发内容
3.1 总线设备驱动模型
总线将设备和驱动绑定,在Linux 内核系统中注册一个设备的时候,会寻找与之对应驱动进行匹配;相反地,系统中注册一个驱动的时 候,会去寻找一个对应的设备进行匹配。匹配的的工作由总线来完成。
在Linux设备中有的是没有对应的物理总线的,但为了适配Linux的总线模型,内核针对这种没有物 理总线的设备开发了一种虚拟总线——platform总线。
按照这个思路,Linux
中的设备和驱动都需要挂接在一种总线上,比如
i2c
总线上的
eeprom
,
eeprom
作为设备,
eeprom
的驱动都挂接在
i2c
驱动上
。但是在嵌入式系统中,
soc
系统一般都会集成
独立的
i2c
控制器,控制器也是需要驱动的,但是再按照设备
-
总线
-
驱动模型进行设计,就会发现无法找
到一个合适总线去挂接控制器设备和控制器驱动了(
i2c
控制器是挂接在
CPU
内部的总线上,而不是
i2c
总线)
,所以
Linux
发明了一种虚拟总线,称为
platform
总线
,
相应的设备称为
platform_device
(控
制器设备),对应的驱动为
platform_driver
(控制器驱动),用
platform
总线来承载这些相对特殊的
系统
。、
所谓的platform_device
并不是与字符设备、块设备和网络设备并列的概念,而是
Linux
系统提供的一种附加手段。
例如,在
S3C6410
处理器中,把内部集成的
I2C
、
RTC
、
SPI
、
LCD
、看门狗等
控制器
都归纳为
platform_device
,而它们本身就是字符设备。我们要记住,
platform
驱动只是在
字符
设备驱动外套一层
platform_driver
的外壳。
引入
platform
模型符合
Linux
设备模型
——
总线、设备、
驱动,设备模型中配套的
sysfs
节点都可以用,方便我们的开发;
设备驱动中引入platform
概念,隔离
BSP
和驱动。在
BSP
中定义
platform
设备和设备使用的资源、 设备的具体匹配信息,而在驱动中,只需要通过API
去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。
(BSP(Board Support Package,板级支持包)是嵌入式系统中一个重要的组成部分,它位于操作系统和硬件之间,主要目的是为了支持操作系统,使之能够更好地运行于硬件主板。)
一边的“device”
结构体和另一边的
“
较稳定的
drivice
代码
”
的联系:
“device_add()”
除了将
“devcie”
结 构放到 bus
的
“dev
链表
”
之外,还会从另一边的
“drv”
链表中取表元即某个
“driver”
结构,用总线里的一个 (.match
)函数来作比较,看另一边的
“driver”
是否支持一边的
“device”
。若是能够支持,则接着调用软 件驱动部分的“.probe”
函数。
"driver_register()"会将“bus_drv_dev”
模型中的较稳定代码
“driver”
结构体放到虚拟总线的某个链表 (drv
链表)中。从另一边的
“dev”
链表中取出每一个
“device”
结构用
bus
中的
“.match”
函数来作比较, 若支持则调用“.probe”
函数。左右两个注册就建立起来的一种机制。在
“.probe”
函数中做的事件由自已决 定,打印一句话,或注册一个字符设备,再或注册一个“input_dev”
结构体等等都是由自已决定。强制的 把一个驱动程序分为左右两边这种机制而已,可以把这套东西放在任何地方,这里的“driver”
只是个结构体不要被这个名字迷惑,“device”
也只是个结构体,里面放什么内容都是由自已决定的。
3.2 输入子系统分析
每个硬件都有一个input_dev
结构体,每个软件都有一个
input_handler
结构体。
input_dev
和
input_handler
分别通过
input_register_device(),input_register_handler()
向核心层注册硬件和软件。
软件和硬件都需要注册
从input_dev
方向分析:
input
设备在增加到
input_dev_list
链表上之后,会查找
input_handler_list 事件处理链表上的handler
进行匹配,这里的匹配方式与总线设备驱动模型的
device
和
driver
匹配过程很 相似,所有的input_device
都挂在
input_dev_list
上,所有类型的事件都挂在
input_handler_list
上,进 行“
匹配相亲
”
。如果匹配上了,就调用
input_handler
的
connect
函数进行连接。设备就是在此时注册的。
从input_handler
方向分析:将
handler
挂到链表
input_handler_list
下,然后遍历
input_dev_list
链 表,
查找并匹配输入设备对应的事件处理层,如果匹配上了,就调用
connect
函数进行连接,并创建 input_handle结构。 所以,
不管新添加
input_dev
还是
input_handler,
都会进入
input_attach_handler()
判断两者
id
是否有 支持,
若两者支持便进行连接。
3.3 驱动的分层和分离思想
分层是从外设功能来看的,尽可能高内聚低耦合,尽可能少的去修改外设硬件的一些参数就可以匹配到相应的驱动。
分离是为了适配多个平台和设备。这个就是驱动的分隔,也就是将主机驱动和设备驱动分隔开来
,比如I2C
、
SPI
等等都会采用驱动分隔的方式来简化驱动的开发。在实际的驱动开发中,一般
I2C
主机控 制器驱动已经由半导体厂家编写好了,而设备驱动一般也有设备器件的厂家编写好了,我们只需要提供设备信 息即可,比如I2C
设备的话提供设备连接到了哪个
I2C
接口上,
I2C
的速度是多少等等。相当于将设备信息从设备驱动中剥离开来,驱动使用标准方法去获取到设备信息(
比如从设备树中获取到设备信息
)
,然后 根据获取到的设备信息来初始化设备。 这样就相当于驱动只需要负责驱动,设备只需要设备,想办法将两者进行匹配即可。这个就是Linux
中的总线
(bus)
、驱动
(driver)
和设备(device)模型,也就是常说的驱动分离。总线就是驱动和设备信息的月老,负责给两者牵线搭桥。如下图所示。
3.4 字符设备驱动模型
3.4.1 源码分析
1.创建一个class类
2.创建类的设备
3.open函数:初始化按键,配置按键的寄存器,例如输入模式
4.read函数:读取按键的值,上传给用户层
5.文件操作结构体:将拥有者,open函数,read函数绑定联系一块;
6.保存主设备号
7.init函数:创建驱动,创建类名,申请虚拟地址,配置寄存器
8. exit函数:卸载驱动,卸载类设备,卸载类,注销虚拟地址
9. 函数许可证
3.4.2 框架
1
写
file_oprations
结构体,
second_drv_open
函数,
second_drv_read
函数
2
写入口函数
,
并自动创建设备节点,修饰入口函数
3
写出口函数,并自动注销设备节点,修饰出口函数
4
写
MODULE_LICENSE(“GPL v2”)
声明函数许可证
5
在入口函数中,利用
class_create
和
class_device_create
自动创建设备节点。在出口函数中
,
利用 class_destroy和
class_device_unregister
注销设备节点。
3.4.3 .在框架中实现硬件操作
1.看原理图,确定硬件结构和寄存器
2.看数据手册,配置寄存器的值
按键0~3
分别是
GPF0
,
GPF2
,
GPG3
,
GPG11
。由于是使用查询模式,并不是外部中断模式。所以 配置 GPFCON(0x56000050)
的位
[0:1]
、位
[4:5]
等于
0x00(
输入模式
)
。
GPGCON(0x56000060)
的位 [6:7]、位
[22:23]
等于
0x00
。
3.写代码
init入口函数中使用
ioremap()
函数映射寄存器虚拟地址
exit出口函数中使用
iounmap()
函数注销虚拟地址
open函数中配置
GPxCON
初始化按键
read函数中先检查读出的字符是否是
4
个
,
然后获取
GPxDAT
状态
,
用
key_vals[4]
数组保存
4
个按键值
,
最后使用
copy_to_user(buf, key_vals,sizeof(key_vals))
上传给用户层
4. 写测试程序
用法就是./ Secondtext
使用read(fd,val,sizeof(val));
函数读取内核层的数据
3.5 LCD驱动模型
3.5.1 LCD驱动模型框架
写个
LCD
驱动入口函数
,
需要以下
4
步
1)
分配一个
fb_info
结构体
: framebuffer_alloc();
2)
设置
fb_info
3)
设置硬件相关的操作
4)
使能
LCD,
并注册
fb_info: register_framebuffer()
主要使用以下分配(释放)内存函数:(直接分配地址和大小)
3.5.2 fb_info
注册:(实则给结构体分配内存)
反注册:(释放内存)
fb_info
结构体如下:可变和固定的参数,操作函数,显存的虚拟起始地址和地址长度
3.5.3 驱动init入口函数
1)
分配一个
fb_info
结构体
2)
设置
fb_info
2.1)
设置固定的参数
fb_info-> fix
。
id,lcd
的名字
2.2)
设置可变的参数
fb_info-> var
。可见屏幕一行有多少个像素点,虚拟屏幕一行有多少个像素
点,每个像素的位数即
BPP,
比如
:RGB565
则填入
16
2.3)
设置操作函数
fb_info-> fbops
2.4)
设置
fb_info
其它的成员,
my_lcdfb_setcolreg
调色板,
cfb_copyarea
复制数据
3)
设置硬件相关的操作
3.1)
配置
LCD
引脚
GPBcon = ioremap(0x56000010, 8)
;
GPBdat = GPBcon+1
;
3.2)
根据
LCD
手册设置
LCD
控制器
VSYNC HSYNC
等参数
3.3)
分配显存
(framebuffer),
把地址告诉
LCD
控制器和
fb_info
4)
开启
LCD,
并注册
fb_info: register_framebuffer()
4.1)
直接在
init
函数中开启
LCD(
后面讲到电源管理
,
再来优化
)
控制
LCDCON5
允许
PWREN
信号
,
然后控制
LCDCON1
输出
PWREN
信号
,
输出
GPB0
高电平来开背光
,
4.2)
注册
fb_info
在驱动
exit
出口函数中
:
1)
卸载内核中的
fb_info
2)
控制
LCDCON1
关闭
PWREN
信号
,
关背光
,iounmap
注销地址
3)
释放
DMA
缓存地址
dma_free_writecombine()
4)
释放注册的
fb_info
3.6 触摸屏驱动
3.6.1 触摸屏原理介绍
3.6.2 触摸屏驱动框架
3.6.3 触摸屏的一些重要参数
3.6.4 复合中断源
S3C2440A将中断源分为两级:中断源和子中断源,中断源里包含单一中断源和复合中断源,复合中断源是子中断源的复合信号。如实时时钟中断,该硬件只会产生一种中断,它是单一中断源,直接将 其中断信号线连接到中断源寄存器上。
3.6.5 init入口函数
3.6.6 其他函数
3.6.7 触摸屏驱动代码
1.设置一些结构体指针,adc时钟,定时器
2. 设置寄存器,启动中断
3. 启动ADC转换函数
4. 快速排序函数(将得到的值排序)
5. 判断误差函数
6. 定时器函数,实现触摸滑动功能
7. 入口函数:申请input_dev,设置input_dev参数,注册input_dev,设置触摸屏相关硬件:开启ADC时钟,设置寄存器ADCCON分频,设置中断,设置寄存器ADCDLY,初始化定时器,开启中断
8.出口函数
9.许可证
3.7 常见的调试方法及技巧总结
4. 中断相关
4.1 linux中内核空间及用户空间的区别
操作系统的核心是内核(kernel)
,它独立于普通的应用 程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的 操作系统一般都强制用户进程不能直接操作内核。
在 CPU
的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如
清内存、设置时
钟
等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。 所以,CPU
将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模
块使用,普通应用程序只能使用那些不会造成灾难的指令。比如
Intel
的
CPU
将特权等级分为
4
个级 别:Ring0~Ring3
。 其实 Linux
系统只使用了
Ring0
和
Ring3
两个运行级别
(Windows
系统也是一样的
)
。当进程运行在 Ring3 级别时被称为运行在用户态,而运行在
Ring0
级别时被称为运行在内核态。
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
既然用户态的进程必须切换成内核态才能使用系统的资源,那么我们接下来就看看进程一共有多少 种方式可以从用户态进入到内核态。概括的说,有三种方式:系统调用、软中断和硬件中断
。
在硬件之上,内核空间中的代码控制了硬件资源的使用权,用户空间中的代码只有通过内核暴露的系统调用接口(System Call Interface)
才能使用到系统中的硬件资源。其实,不光是
Linux
,
Windows 操作系统的设计也是大同小异。
4.2 用户空间与内核通信方式有哪些
1.使用API:最常见
2..使用proc文件系统:和sysfs文件系统类似,也可以作为内核空间和用户空间交互的手段。
/proc 文件系统是一种虚拟文件系统,通过他可以作为一种
linux
内核空间和用户空间的。与普通文 件不同,这里的虚拟文件的内容都是动态创建的。
使用/proc
文件系统的方式很简单。调用
create_proc_entry
,返回一个
proc_dir_entry
指针,然后去 填充这个指针指向的结构就好了。
3.使用sysfs文件系统+kobject
在内核中注册的kobject都对应着
sysfs
系统中的一个目录。可以通过读取根目录下的
sys
目录中的文件来获得相应的信息。除了sysfs
文件系统和
proc
文件系统之外,一些其他的虚拟文件系统也能同样达到这个效果。
4.netlink
netlink socket提供了一组类似于
BSD
风格的
API
,用于用户态和内核态的
IPC。
5.文件
应该说这是一种比较笨拙的做法,不过确实可以这样用。当处于内核空间的时候,直接操
作文件,将想要传递的信息写入文件,然后用户空间可以读取这个文件便可以得到想要的数据了。
6.使用mmap系统调用
可以将内核空间的地址映射到用户空间。
7. 信号
从内核空间向进程发送信号。当用户程序出现重大错误,内核发送信号杀死相应进程。
4.3 linux中中断的实现机制,tasklet与workqueue的区别及底层实现区别?为什么要区分上半部和下半部?
4.3.1 tasklet与workqueue的区别
Tasklet和workqueue是Linux内核中两种不同的机制,用于处理中断下半部操作或延迟执行的任务。
Tasklet 是一种轻量级的机制,用于处理中断上下文中无法立即完成的工作。它们运行在原子上下文中,不能休眠,因此不能执行可能引起睡眠的任何操作。Tasklet设计为不可重入,确保同一类型的tasklet不会在多个CPU上并发执行。
Workqueue 是一种更为灵活的机制,允许将工作项(work item)排队,由内核线程异步执行。与tasklet不同,workqueue可以在进程上下文中运行,因此可以休眠、重新调度,甚至访问用户空间。Workqueue适合处理可能需要较长时间完成的任务,或者需要执行阻塞操作的任务
总结:tasklet运行于中断上下文,不允许阻塞 、休眠,而
workqueue
运行与进程上下文,可以休眠和阻塞。
4.3.2 为什么要区分上半部和下半部?
中断服务程序异步执行,可能会中断其他的重要代码,包括其他中断服务程序。因此,为了避免被 中断的代码延迟太长的时间,中断服务程序需要尽快运行,而且执行的时间越短越好,所以中断程序只作必须的工作,其他工作推迟到以后处理。所以Linux
把中断处理切为两个部分:上半部和下半部。上半部就是中断处理程序,它需要完成的工作越少越好,执行得越快越好,一旦接收到一个中断,它就立即开始执行。
像对时间敏感、与硬件相关、要求保证不被其他中断打断的任务往往放在中断处理程序中执 行
;而剩下的与中断有相关性但是可以延后的任务,如
对数据的操作处理,则推迟一点由下半部完成。下半部分延后执行且执行期间可以相应所有中断,这样可使系统处于中断屏蔽状态的时间尽可能的短, 提高了系统的响应能力。实现了程序运行快同时完成的工作量多的目标。
由于软中断必须使用可重入函数,这就导致设计上的复杂度变高,作为设备驱动程序的开发者来说,增加了负担。而如果某种应用并不需要在多个CPU
上并行执行,那么软中断其实是没有必要的。因此诞生了弥补以上两个要求的tasklet
。它具有以下特性:
a
)一种特定类型的
tasklet
只能运行在一个
CPU
上,不能并行,只能串行执行。
b
)多个不同类型的
tasklet
可以并行在多个
CPU
上。
c
)软中断是静态分配的,在内核编译好之后,就不能改变。但
tasklet
就灵活许多,可以在运行时改 变(比如添加模块时)。
顶半部用于完成尽量少的比较紧急的功能
,它往往只是简单地
读取寄存器中的中断状态
,并在清除 中断标志后就进行“
登记中断
”
的工作。
“
登记中断
”
意味着将底半部处理程序挂到该设备的底半部执行队列 中去。这样,顶半部执行的速度就会很快,从而可以服务更多的中断请求。
现在,中断处理工作的重心就落在了底半部的头上,需用它来完成中断事件的绝大多数任务。底半 部几乎做了中断处理程序所有的事情
,而且可以被
新的中断打断
,这也是底半部和顶半部的最大不同, 因为顶半部往往被设计成不可中断
。底半部相对来说并不是非常紧急的,而且相对比较耗时,不在硬件 中断服务程序中执行。
尽管顶半部、底半部的结合能够善系统的响应能力,但是,僵化地认为Linux设备驱动中的中断处理 一定要分两个半部则是不对的。如果中断要处理的工作本身很少,则完全可以直接在顶半部全部完成
。
其他操作系统中对中断的处理也采用了类似于 Linux的方法,真正的硬件中断服务程序都斥尽量 短。因此,许多操作系统都提供了中断上下文和非中断上下文相结合的机制,将中断的耗时工作保留到 非中断上下文去执行。
4.3.3 linux中断的响应执行流程?中断的申请及何时执行(何时执行中断处理函数)?
中断的响应流程:cpu
接受中断
->
保存中断上下文跳转到中断处理历程
->
执行中断上半部
->
执行中断下半部->
恢复中断上下文。
中断的申请request_irq
的正确位置:应该是在第一次打开 、硬件被告知终端之前
驱动中操作物理绝对地址为什么要先ioremap?
内核没有办法直接访问物理内存地址,必须先通过ioremap获得对应的虚拟地址
4.4 实现中断下半部的三种方法
4.4.1 软中断
软中断( Softirq
)也是一种传统的底半部处理机制,它的执行时机通常是
顶半部返回的时候
, tasklet是基于软中断实现的,因此也运行于软中断上下文。
在Linux
内核中,用
softing_action
结构体表征一个软中断,这个结构体包含软中断处理函数指针和 传递给该函数的参数。使用 open_softirq()函数可以注册软中断对应的处理函数,而raise_softirq
()函数可以触发一个软中断。
软中断和 tasklet
运行于软
中断上下文
,仍然属于原子上下文的一种,而工作队列则运行于
进程上下
文
。因此,在软中断和
tasklet
处理函数中
不允许睡眠
,而在
工作队列处理函数中允许睡眠
。
local_bh_disable()和 llocal_bh_enable
()是内核中用于禁止和使能软中断及
tasklet
底半部机制的函数
4.4.2 tasklet
tasklet函数模版
4.4.3 工作队列
工作队列早期的实现是在每个CPU
核上创建一个
worker
内核线程,所有在这个核上调度的工作都在 该worker
线程中执行,其并发性显然差强人意。在
Linux 2.6.36
以后,转而实现
“Concurrency managedworkqueues”,简称
cmwq
,
cmwq
会自动维护工作队列的线程池以提高并发性,同时保持了 API的向后兼容。
4.5 软中断和硬中断的区别
4.5.1 硬中断
1.
硬中断是由
硬件产生
的,比如,像磁盘,网卡,键盘,时钟等。
每个设备或设备集都有它自己的
IRQ
(中断请求)
。基于
IRQ
,
CPU
可以将相应的请求分发到对应的硬件驱动上(注:硬件驱动通常是内核中的一个子程序,而不是一个独立的进程)。
2.
处理中断的驱动是需要运行在
CPU
上的,因此,当中断产生的时候,
CPU
会中断当前正在运行的任务,来处理中断。在有多核心的系统上,一个中断通常只能中断一颗CPU
(也有一种特殊的情况,就 是在大型主机上是有硬件通道的,它可以在没有主CPU
的支持下,可以同时处理多个中断。)。
3.
硬中断可以直接中断
CPU
。它会引起内核中相关的代码被触发。对于那些需要花费一些时间去处理的进程,中断代码本身也可以被其他的硬中断中断。
4.
对于时钟中断,内核调度代码会将当前正在运行的进程挂起,从而让其他的进程来运行。它的存
在是为了让调度代码(或称为调度器)可以调度多任务。
4.5.2 软中断
1.
软中断的处理非常像硬中断。然而,它们仅仅是由
当前正在运行的进程
所产生的。
2.
通常,软中断是一些对
I/O
的请求。这些请求会调用内核中可以调度
I/O
发生的程序。
对于某些设
备,
I/O
请求需要被立即处理,而磁盘
I/O
请求通常可以排队并且可以稍后处理
。
3. 软中断仅与内核相联系。而内核主要负责对需要运行的任何其他的进程进行调度。
4.
软中断并不会直接中断
CPU
。也
只有当前正在运行的代码(或进程)才会产生软中断。
4.5.3 硬中断、软中断和信号的区别
硬中断是外部设备对
CPU
的中断
,软中断是
中断底半部的一种处理机制
,而信号则是由
内核(或其
他进程)对某个进程的中断
。
需要特别说明的是,软中断以及基于软中断的tasklet如果在某段时间内大量出现的话,内核会把后 续软中断放入ksoftirqd
内核线程中执行。总的来说,
中断优先级高于软中断
,
软中断又高于任何一个线
程
。软中断适度线程化,可以缓解高负载情况下系统的响应。
4.6 linux中RCU原理
RCU机制是
Linux2.6
之后提供的一种数据一致性访问的机制
RCU的思路实际上很简单,如下所示:
a.
对于读操作,可以直接对共享资源进行访问,但是前提是需要
CPU
支持访存操作的原子化,现代 CPU对这一点都做了保证。
b .
对于写操作,其需要将原来的老数据作一次备份(
copy
),然后对备份数据进行修改,修改完毕 之后再用新数据更新老数据,更新老数据时采用了rcu_assign_pointer
()宏,在该函数中首先屏障一 下memory
,然后修改老数据。
c.
在
RCU
机制中存在一个垃圾回收的
daemon
,当共享资源被
update
之后,可以采用该
daemon
实
现老数据资源的回收。
RCU思想是比较简单的,其核心内容紧紧围绕
“
写时拷贝
”
,采用
RCU
机制,能够保证在读写操作共享资源时,基本不需要取锁操作,能够在一定程度上提升性能。但是该机制的应用是有条件的,对于读多写少的应用,机制的开销比较小,性能会大幅度提升,但是如果写操作较多时,开销将会增大,性能 不一定会有所提升。总体来说,RCU
机制是对
rw_lock
的一种优化。
4.7 linux中软中断的实现原理
中断的执行的过程分为上下两部分,但是下半部分执行过程中有两个缺陷:(1
)在任意一时
刻,系统只能有一个
CPU
可以执行下半部代码,以防止两个或多个
CPU
同时来执行下半部函数而相互干 扰。因此下半部代码的执行是严格“
串行化
”
的。(
2
)下半部函数不允许嵌套。
整个softirq
机制的设计与实现中自始自终都贯彻 了一个思想:“
谁触发,谁执行
”
(
Who marks
,
Who runs
),也即触发软中断的那个
CPU
负责执行它所触发的软中断,而且每个CPU
都由它自己的软中断触发与控制机制。
软中断的工作过程模拟了硬中断的处理过程:
a
、当某一软中断事件发生后,首先需要设置对应的中断标记位,触发中断、设置软中断状态。
b、然后唤醒守护线程去检测中断状态寄存器
c
、如果通过查询发现某一软中断事务发生之后,那么通过软中断向量表调用软中断服务程序
action
()。
这就是软中断的过程,与硬件中断唯一不同 的地方是从中断标记到中断服务程序的映射过程。在 CPU的硬件中断发生之后,
CPU
需要将硬件中断请求通过向量表映射成具体的服务程序,这个过程是硬 件自动完成的,但是软中断不是,其需要守护线程去实现这一过程,这也就是软件模拟的中断故称之为软中断。
5. 内核中的并发和竞争
5.1 简介
Linux系统 是个多任务操作系统,会存在多个任务同时访问同一片内存区域的情况,这些任务可能会相互覆盖这段 内存中的数据,造成内存数据混乱。
Linux
系统并发产生的原因很复杂,总结一下有下面几个主要原因:
1.
多线程并发访问,
Linux
是多任务(线程)的系统,所以多线程访问是最基本的原因。
2.
抢占式并发访问,内核代码是可抢占的,因此,我们的驱动程序代码可能在任何时候丢失对处理器的独占。
3.
中断程序并发访问,设备中断是异步事件,也会导致代码的并发执行。
4. SMP
(多核)核间并发访问,现在
ARM
架构的多核
SOC
很常见,多核
CPU
存在核间并发访问。正在 运行的多个用户空间进程可能以一种令人惊讶的组合方式访问我们的代码,SMP
系统甚至可在不同 的处理器上同时执行我们的代码。
当两个执行线程需要访问相同的数
据结构
(或硬件资源)时,混合的可能性就永远存在。因此在设计自己的驱动程序时,就应该避免资源 的共享。如果没有并发的访问,也就不会有竞态的产生
。因此,仔细编写的内核代码应该具有
最少的共
享
。这种思想的最明显应用就是
避免使用全局变量
。如果我们将资源放在多个执行线程都会找到的地方 (临界区),则必须有足够的理由。
5.2 原子操作
在内核中所说 的原子操作表示这一个访问是一个步骤,必须一次性执行完,不能被打断,不能再进行拆分。
例如,在多线程访问中,我们的线程一对a进行赋值操作,
a=1
,线程二也对
a
进行赋值操作
a=2
, 我们理想的执行顺序是线程一先执行,线程二再执行。但是很有可能在线程一执行的时候被其他操作打断,使得线程一最后的执行结果变为 a=2
。要解决这个问题,必须保证我们的线程一在对数据访问的过 程中不能被其他的操作打断,一次性执行完成
5.3 自旋锁
自旋锁只有锁定和解锁两个状态。当我们进入拿上钥匙进入厕所,这就相 当于自旋锁锁定的状态,期间谁也不可以进来。当第二个人想要进来,这相当于线程B想要访问这个共 享资源,但是目前不能访问,所以线程B
就一直在原地等待,一直查询是否可以访问这个共享资源。当
我们从厕所出来后,这个时候就
“
解锁
”
了,只有再这个时候线程
B
才能访问。
自旋锁也是这样的,如果线程A
持有
自旋锁时间过
长,显然会浪费处理器的时间,降低了系统性能
。我们知道
CPU
最伟大的发明就在于多线程操作,这个时候让线程B
在这里傻傻的不知道还要等待多久,显然是不合理的。因此,如果
自旋锁只适合短期持
有
,如果遇到需要长时间持有的情况,我们就要换一种方式了(下文的互斥体)。
自旋锁是主要为了多处理器系统
设计的。对于
单处理器且内核不支持抢占的系统
,一旦进入了自旋 状态,则会永远自旋下去。因为,没有任何线程可以获取CPU
来释放这个锁。因此,在单处理器且内核 不支持抢占的系统中,自旋锁会被设置为空操作
。
以上列表中的函数适用于
SMP
或支持抢占的单
CPU
下线程之间的并发访问,也就是用于线程与线程 之间,被自旋锁保护的临界区一定不能调用任何能够引起睡眠和阻塞(其实本质仍然是睡眠)的
API
函数,否则的话会可能会导致死锁现象的发生。
5.4 信号量
信号量和自旋锁有些相似,不同的是信号量会发出一个信号告诉你还需要等多久。因此,不会出现 傻傻等待的情况。比如,有100个停车位的停车场,门口电子显示屏上实时更新的停车数量就是一个信 号量。这个停车的数量就是一个信号量,他告诉我们是否可以停车进去。当有车开
信号量具有以下特点:
1.
因为信号量可以使等待资源线程进入休眠状态,因此适用于那些
占用资源比较久
的场合。
2.
因此信号量不能用于中断中,因为信号量会引起休眠,
中断不能休眠
。
3.
如果共享资源的持有时间比较短,那就不适合使用信号量了,因为频繁的休眠、切换线程引起的开
销要远大于信号量带来的那点优势
。
5.5 互斥体
互斥体表示一次只有一个线程访问共享资源,不可以递归申请互斥体
。 信号量也可以用于互斥体,当信号量用于互斥时(即避免多个进程同时在一个临界区中运行),信 号量的值应初始化为1.
这种信号量在任何给定时刻只能由单个进程或线程拥有。在这种使用模式下,一 个信号量有时也称为一个“
互斥体(
mutex
)
”
,它是互斥(
mutual exclusion
)的简称。
Linux内核中
几平所有的信号量均用于互斥
。
6. Linux内核中的阻塞和异步通知机制
6.1 阻塞/非阻塞简介
阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程
直到满足可操作的条件后再进行操 作。被挂起的进程进入睡眠状态
,被从调度器的运行队列移走,
直到等待的条件被满足
。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它要么放弃,要么不停地查询
,直至可以进行操作为止。
6.2 等待队列简介
等待队列是内核中一个重要的数据结构。阻塞方式访问设备时,如果设备不可操作,那么进程就会进入休眠状态
。等待队列就是来完成进程休眠操作的一种数据结构。
6.3 轮询
当应用程序以非阻塞的方式访问设备时,会一遍一遍的去查询我们的设备是否可以访问,这个查询 操作就叫做轮询。内核中提供了poll
,
epoll
,
select
函数来处理轮询操作。当应用程序在上层通过 poll,
epoll
,
select
函数来查询设备时,驱动程序中的
poll
,
epoll
,
select
函数就要在底层实现查询,如 果可以操作的话,就会从读取设备的数据或者向设备写入数据。
6.3.1 select
6.3.2 poll
在单个线程中, select
函数能够监视的文件描述符数量有最大的限制,一般为
1024
,可以修改内核 将监视的文件描述符数量改大,但是这样会降低效率!这个时候就可以使用poll
函数,
poll
函数本质上 和 select
没有太大的差别,但是
poll
函数
没有最大文件描述符限制
,
Linx
应用程序中
poll
函数原型如下所示:
6.3.3 epoll
传统的 selcet
和
poll
函数都会随着所监听的
fd
数量的增加,出现效率低下的问题,而且
poll
函数每次 必须遍历所有的描述符来检查就绪的描述符,这个过程很浪费时间。为此,epoll
因运而生,
epoll
就是为 处理大并发而准备的,一般常常在网络编程中使用epoll
函数。应用程序需要先使用
epoll_create
函数创 建一个 epoll
句柄,
epoll create
函数原至如下
.
6.4 异步通知
6.4.1 简介
阻塞与非阻塞访问、poll
函数提供了较好的解决设备访问的机制,但是如果有了异步通知,整套机 制则更加完整了。
异步通知的意思是:一旦设备就绪,则主动通知应用程序
,这样应用程序根本就不需要查询设备状 态,这一点非常类似于硬件上“
中断
”
的概念,比较准确的称谓是
“
信号驱动的异步
I/O”
。
信号是在软件层
次上对中断机制的一种模拟
,在原理上,
一个进程收到一个信号与处理器收到一个中断请求可以说是一
样的
。信号是异步的,一个进程不必通过任何操作来等待信号的到达,事实上,进程也不知道信号到底 什么时候到达。
阻塞I/O意味着一直等待设备可访问后再访问,非阻塞
I/O
中使用
poll
()意味着查询设备是否可访 ,而异步通知则意味着设备通知用户自身可访问,之后用户再进行I/O
处理。由此可见,这几种
I/O
方式可以相互补充。
6.4.2 Linux信号
异步通知的核心就是信号,在 arch/xtensa/include/uapi/asm/signal.h文件中定义了Linux所支持 的所有信号
6.4.3 异步通知代码
类似于中断
6.4.4 应用程序对异步通知的处理