前言:
1,明确:linux内核一些相关概念
“任务”:在linux内核中,不仅包括进程还包括中断,中断不隶属于进程,独立于进程
“中断”:包括硬件中断(外设发送的中断电信号)和软中断(svc/swi指令)
"优先级":衡量任务获取CPU资源的一种能力,优先级越高获取CPU资源的能力就越高,这个任务就可以越早运行
任务优先级:硬件中断的优先级高于软中断的优先级;软中断的优先级高于进程;进程之间有优先级之分;软中断同样也有优先级;硬件中断无优先级。
“休眠”:这个词只能适用于进程,在中断的世界里没有休眠;进程休眠是指进程释放CPU资源给其它进程使用由于中断不隶属于进程,不参与进程的调度如果中断休眠,结果是死机!
进程上下文:就是进程的整个处理的过程(进程的创建,进程之间的调度,进程的抢占,进程的销毁)
中断上下文:就是中断的整个处理过程(跳转到异常向量表,保护现场,处理,恢复现场)
2,内核功能划分
(1)进程管理:内核负责创建和销毁进程,并处理它们与外部世界的联系(输入和输出). 不同进程间通讯(通过信号, 管道,或者进程间通讯原语)对整个系统功能 来说是基本的, 也由内核处理. 另外, 调度器,控制进程如何共享 CPU, 是进程管理的一部分. 更通常地, 内核的进程管理活动实现了多个进程在 一个单个或者几个 CPU 之上的抽象.
(2)内存管理:计算机的内存是主要的资源,处理它所用的策略对系统性能是至关重要的. 内核为所有进程的每一个都在有限的可用资源上建立了一个虚拟地址空间.内核的不同部分与内存管理子系统通过一套函数调用交互, 从简单的malloc/free 对到更多更复杂的功能.
(3)文件系统:Unix 在很大程度上基于文件系统的概念; 几乎 Unix 中的任何东西都可 看作一个文件. 内核在非结构化的硬件之上建立了一个结构化的文件系统, 结果是文件的抽象非常多地在整个系统中应用.另外, Linux 支持多个文 件系统类型, 就是说, 物理介质上不同的数据组织方式. 例如, 磁盘可被 格式化成标准 Linux 的 ext3 文件系统, 普遍使用的FAT 文件系统, 或 者其他几个文件系统.
(4)设备控制:几乎每个系统操作最终都映射到一个物理设备上.除了处理器, 内存和非 常少的别的实体之外, 全部中的任何设备控制操作都由特定于要寻址的设 备相关的代码来进行. 这些代码称为设备驱动.内核中必须嵌入系统中出 现的每个外设的驱动, 从硬盘驱动到键盘和磁带驱动器.内核功能的这个 方面是本书中的我们主要感兴趣的地方.
(5)网络功能:网络必须由操作系统来管理,因为大部分网络操作不是特定于某一个进程: 进入系统的报文是异步事件.报文在某一个进程接手之前必须被收集, 识 别, 分发. 系统负责在程序和网络接口之间递送数据报文, 它必须根据程 序的网络活动来控制程序的执行. 另外, 所有的路由和地址解析问题都在 内核中实现.
进入正题:linux内核对于硬件中断处理函数的要求
(1).linux内核要求硬件中断处理函数执行的速度要快!如果硬件中断处理函数长时间的占用CPU资源,会导致其它任务无法获取到CPU资源,无法投入运行,势必导致影响系统的并发和响应能力!
(2).这仅仅是内核的要求,但是有很多场合并不能满足这种要求,那就代表会影响系统的并发和响应能力,内核如何解决这种问题呢?
答:linux内核对应中断情形,采用顶半部和底半部机制
一,linux内核中断编程之顶半部机制
特点:
(1)本质就是原先的中断处理函数,只是现在做原先中断处理函数中比较紧急,耗时较短的内容,这样就可以及时释放CPU资源
(2)顶半部在执行期间不允许被打断!不允许发生CPU资源的切换!
二.linux内核中断编程之底半部
特点:
(1)底半部执行原先中断处理函数中不紧急,耗时较长的内容
(2)底半部执行期间可以被高优先级的任务打断,允许CPU资源发生切换
底半部3种实现方法:tasklet ; 工作队列 ; 软中断 ;
1,底半部机制之tasklet
(1)本质:就是延后执行的一种手段而已!
(2)特点:<1>基于软中断实现<2>不能进行休眠操作
<3>有对应的延后处理函数,这个函数中做原先中断处理函数中不紧急,
耗时较长的内容也就代表tasklet的延后处理函数不能进行休眠操作
(3)数据结构:struct tasklet_struct
{
void (*func) (unsigned long data);
unsigned long data;
};
(4)功能参数:描述tasklet
func:延后处理函数,做原先中断处理函数中耗时较长,不紧急的内容 不能进行休眠操作,CPU会在"适当"的时候执行此函数
“适当”:此函数如果要执行,说明当时它的优先级最高!
形参data:保存传递的参数信息,一般传递指针使用时注意数据类型的转换!
data:给延后处理函数传递的参数,一般传递指针
编程使用:
<1>定义初始化一个tasklet对象
int g_data = 0x55;
DECLARE_TASKLET(btn_tsk, //对象名 btn_task_func, //延后处理函数 (unsigned long)&data);//传递的参数
<2>编写延后处理函数
void btn_task_func (unsigned long data)
{
//不紧急,耗时较长的内容
//千万不能进行休眠操作
}
<3>向内核登记延后处理函数,一旦登记完成,内核会在适当的 时候执行延后处理函数 tasklet_schedule(&对象);
那么在什么地方登记呢?
答:1.如果有顶半部(中断处理函数),一般在顶半部进行登记 2.如果没有顶半部,根据需求登记即可,位置不限!
明确:底半部本质就是延后执行,将某个事情放在以后去执行而已!
2,linux内核中断底半部机制之工作队列
(1)本质:同样是延后执行的一种手段!
(2)目的:就是解决tasklet的延后处理函数不能休眠的问题在有些场合,可能需要在底半部进行休眠操作,
而 tasklet不能满足这种要求,此时可以选择工作队列来实现延后执行,它允许休眠
(3)特点:<1>.它的延后处理函数可以进行休眠操作 <2>.它的延后处理函数的优先级低于tasklet和硬件中断
(4)数据结构: struct work_struct
{
void (*func)(struct work_struct *work);
};
func:延后处理函数,工作在进程上下文可以进行休眠操作, 形参work指针指向驱动定义初始化的工作队列对象就是指向自己!
大名鼎鼎的内核宏:container_of
#define container_of(ptr, type, member) ({const typeof(((type *)0)->member) * __mptr = (ptr); (type *)((char *)__mptr - offsetof(type, member)); })
#endif
(5)编程使用:
<1>初始化工作对象,指定一个延后处理函数
INIT_WORK(&工作对象, 延后处理函数);
<2>向内核登记延后处理函数,一旦登记完成,内核在"适当"的时候 执行此函数
schedule_work(&工作对象);
注意:在什么地方登记?1.如果有顶半部,一般在顶半部去登记2.如果没有顶半部,具体在哪里登记,由需求来定
(6)明确:工作队列仅仅是延后执行的一种手段,即使没有顶半部, 照样玩,可以单独使用!
总结:
<1>.tasklet基于软中断实现,工作在中断上下文,所以不能进行休眠操作
<2>.工作队列基于进程实现,工作在进程上下文,所以可以进行休眠操作
<3>.如果有休眠的场合,必须使用工作队列,如果没有休眠呢,可以选择tasklet,也可以选择工作队列,只是前者的效率要高点
3,linux内核底半部机制之软中断
(1)本质:也是延后执行的一种手段
(2)特点:
<1>软中断的延后处理函数工作在中断上下文所以不能进行休眠操作
<2>软中断的延后处理函数同一时刻可以运行在多个CPU上tasklet的延后处理函数只能在同一时刻运行在一个CPU上
软中断的延后处理的效率最高 所以软中断的延后处理函数必须设计成具有可重入性!
可重入函数的特点:
<1>.尽量避免访问全局变量
<2>.如果要访问全局变量,记得要进行互斥访问但是代码执行效率降低!
<3>.软中断的延后处理函数的实现代码必须和zImage编译在一起不能insmod和rmmod
linux内核等待队列机制
1,概念:等待队列中的“等待”是指休眠等待,所以等待队列只适用于进程
“等待”:期待某个事件到来,数据到来, 设备可用 ( 可读或者可写 )
2,本质:linux内核等待队列机制本质就是让进程在内核空间进行休眠操作,根本的原因就是因为外设的处理速度慢于CPU的
处理速度,所以有一个等待的动作!
例如:以CPU读取UART数据为例,谈谈等待队列的重要性,当某个进程获取到CPU资源读取UART数据时,如果发现UART数据没有准备就绪, CPU可以采用轮询的方式读取UART数据,最后的结果是大大降低了CPU的利用率,为了解决这个问题,可以采用中断,当某个进程获取到CPU资源读取UART数据是,如果发现UART数据没有准备继续,当前进程就会释放CPU资源给其它的进程使用,而当前进程将会进入休眠等待状态,当UART数据准备就绪,UART会给CPU发送一个中断信号,触发中断,此时此刻,只需在中断中唤醒之前休眠等待的进程,休眠等待的进程一旦被唤醒,即可继续读取UART数据到用户空间中断的到来也就预示着UART数据的准备OK
3 总结:这个过程CPU资源的切换
(1)“读取UART的进程”获取CPU运行
(2)当他进入休眠等待,CPU资源给“其它进程”
(3)UART数据到来,产生中断,CPU资源给中断
(4)中断唤醒休眠进程,读取UART的进程再次获取CPU资源读取UART数据
问:如何让一个进程在驱动中(内核空间)进入休眠等待呢?等待期望的“事件”到来!答:利用等待队列机制
4,休眠等待的方法
msleep(5000)/ssleep(5)/shcdule_timeout(5*HZ) —— 以上方法休眠特点是必须要休够一定的时间!
schedule();//永久性休眠
如:进程想随时随地休眠,也能够随时随地被唤醒,显然以上那些休眠的方法只能够完成随时随地休眠,但是不能够随时随地被唤醒
切记:再次强调等待队列就可以实现进程在内核空间的随时随地休眠随时随地被唤醒!
5,等待队列 VS 工作队列
工作队列:底半部机制实现的一种方法,是延后执行的一种手段,工作在进程上下文中
等待队列:实现进程在内核空间的随时随地休眠等待事件的到来也能够随时随地得被唤醒
6,等待队列编程方法1: 等待队列的编程实现过程(类比老鹰抓小鸡)
(1)定义初始化一个等待队列头(构造一个鸡妈妈)
注意:等待队列头的个数基于进程访问设备的方式来定
例如:如果有三个读取UART数据的进程,这三个进程就放在一个等待队列中,此时就需要给这个等待队列定义一个等待队列头(构造一个鸡妈妈)如果有三个写UART的进程,这个三个进程就可以放在另一个等待队列中, 此时就需要再定义一个等待队列头(构造一个鸭妈妈)
wait_queue_head_t wq; //定义一个等待队列头对象
init_waitqueue_head(&wq); //初始化等待队列头对象
(2)定义初始化装在休眠进程的容器(构造小鸡)
注意:装载在休眠进程的容器的个数基于进程的个数,一个进程对应一个容器,一个小鸡,一般定义初始化这个容器的时候,它都是局部变量。 “当前进程”:正在获取到CPU资源运行的进程, A进程获取到了CPU资源运行,A进程就是当前进程当A进程的时间片用尽,调度器就会把A进程的CPU资源给B进程,此时B进程就是当前进程。
wait_queque_t wait; //定义初始化一个装载要休眠进程的容器
init_waitqueue_entry(&wait, current); //将当前进程添加到这个wait容器中
current //内核全局指针变量,永远指向当前进程的struct task_struct对象;
struct task_struct
{
pid_t pid;//进程的PID
char comm[TASK_COMM_LEN]; //进程的名称
};
//linux内核用于描述进程或者线程的数据结构,每当创建一个进程或者线程时,内核会帮你用此数据结构定义初始化一个对象来描述你创建的线程或者进程的属性,而current指针就指向当前进程对应的struct task_struct对象!current指向的对象是动态变化!
驱动调试信息:打印进程的名称和PID printk("当前进程[%s][%d]\n", current->comm, current->pid);
(3)将当前要休眠的进程添加到等待队列中(将小鸡放到鸡妈妈后面)
add_wait_queue(&wq, &wait);
注意:此时此刻当前进程还没有进入真正的休眠状态
add_wait_queue_exclusive(&wq, &wait); //顺序添加
(4)设置当前进程将来要休眠的状态
注意:此时此刻,进程还没有进入真正的休眠
//设置为当前进程的休眠状态为可中断的休眠状态
//此休眠状态的进程将来被唤醒的方法有两种:通过kill信号来唤醒 和 通过驱动主动来唤醒
set_current_state(TASK_INTERRUPTIBLE);
或者
//设置为当前进程的休眠状态为不可中断的休眠状态
//此休眠状态的进程将来被唤醒的方法只有一种:
只能是驱动主动唤醒,但是如果接收到了kill信号,不会立即被唤醒将来驱动主动唤醒以后,进程还是要处理之前接收到的信号
set_current_state(TASK_UNINTERRUPTIBLE);
(5)当前进程正式真正的进入休眠状态
schedule( );一旦被调用,当前进程会释放CPU资源,将CPU资源给其它的进程使用,此时代码就停止不前,
当前进程就等待着事件的到来,唤醒这个休眠的进程
(6)一旦事件到来,唤醒休眠的进程,休眠的进程在此获取到 CPU资源,从schedle()返回,代码继续往下执行,设置当前进程的状态为运行态set_current_state(TASK_RUNNING);
(7)然后将被唤醒的进程从等待队列中移除(老鹰干掉小鸡)
remove_wait_queue(&wq, &wait);
(8)如果进程之前设置的休眠状态为可中断的休眠状态,最后最好还是要判断一下被唤醒的原因(要不是驱动主动唤醒,要不是接收到了kill信号)
if ( signal_pending(current) )
{
printk("进程由于接收到了kill信号引起的唤醒");
return -ERESTARTSYS;
}
else
{
printk("进程由于驱动主动唤醒!\n");
//既然是驱动主动唤醒,说明硬件要不可读要不可写反正是硬件准备就绪了!当前进程就可以继续读取数据从硬件或者写入数据到硬件
}
(9)驱动主动唤醒的方法有两种: 接收到了信号 和 驱动主动唤醒
前者在休眠类型为不可中断的休眠进程中,进程是不会立即响应和处理信号,但是休眠类型为可中断的进程会立即响应处理
当访问的设备可用(设备可读或者可写了)此时,硬件设备,势必给CPU发送中断信号,此时可以在中断中来唤醒休眠的进程,这个过程称之为驱动主动唤醒(内核唤醒)
涉及的相关函数:
wake_up(&wq); //驱动主动唤醒wq等待队 有的休眠进程(不管可中断不可中断都唤醒)
或者
wake_up_interruptible(&wq); //驱动主动唤醒wq等待队列中所有的休眠类型为可中断的进程