另外写串口时,是向终端缓冲区当中写入,那么上层的写操作如何知道下层缓冲区中的的数据是否传送完成?用户空间的写串口进程处于什么样的状态?如果是写完缓冲区就睡眠以保证高效的CPU使用率,那么何时才应该醒过来? 由谁负责醒过来?
1. 往tq_timer任务队列中添加一项任务.
根据以上这两个问题,我们来深入代码分析,首先看接收缓冲区中的数据如何上传, 前面已经提到过,接收中断处理完成后,会调用tty_flip_buffer_push(),这个函数完成的功能就是往一系统定义的任务队列当中加入一个任务,下面我们将详细的分析加入的任务最终是如何执行起来的.[任务:这里所讲的任务可以直接理解成为一个相应的回调函数,LINUX下术语称作tasklet]
void tty_flip_buffer_push(struct tty_struct *tty)
{
if (tty->low_latency)
flush_to_ldisc((void *) tty);
else
queue_task(&tty->flip.tqueue, &tq_timer);
}
2. tq_timer的执行路径分析.
tq_timer是一个双链表结构任务队列,每项任务包含一个函数指针成员, 它通过run_task_queue每次将当中的所有任务(其实是一些函数指针)全部调用一次,然后清空队列, 最终的执行tq_timer的是在中断底半的tqueue_bh 中执行,如下:
void tqueue_bh(void)
{
run_task_queue(&tq_timer);
}
在void __init sched_init(void)当中初始化底半的向量如, tqueue_bh初始化在bh_base的TIMER_BH位置,bh_base为一结构很简单的数组,在什么位置调用什么样的了函数基本已经形成默认的习惯:
init_bh(TIMER_BH, timer_bh);
init_bh(TQUEUE_BH, tqueue_bh);
init_bh(IMMEDIATE_BH, immediate_bh);
看看init_bh相当于初始底半的服务程序,非常简单:
void init_bh(int nr, void (*routine)(void))
{
bh_base[nr] = routine;
mb();
}
最终真正的执行bh_base中保存的函数指针的,在bh_action()当中:
static void bh_action(unsigned long nr)
{
…
if (bh_base[nr])
bh_base[nr]();
…
}
关于这里所指出的bh_base, 我们在后面就直接称作bh,意即中断底半所做的事.
3. tq_timer实现所依赖的tasklet.
那么bh_action在什么时候执行呢?bh_action被初始化成bh_task_vec这32个tasklet调用的任务, 因此它的依赖机制是tasklet机制,后面将进行简单介绍.
void __init softirq_init()
{
int i;
for (i=0; i<32; i++)
tasklet_init(bh_task_vec+i, bh_action, i);
….
}
至此已经把任务队列的执行流程及原理分析完成,tasklet是须要激活的,这里我们先指出任务队列是如何激活的,在时钟中断的do_timer()当中会调用mark_bh(TIMER_BH), 来激时钟底半所依赖运行的tasklet,其中bh_task_vec的所有成员的函数指针全部指向bh_action.
static inline void mark_bh(int nr)
{
tasklet_hi_schedule(bh_task_vec+nr);
}
tasklet_hi_schedule的功能就是往tasklet当中加入一个新的tasklet.
4. tasklet的机制简单分析.
讲到tasklet,我们才与我们真正要讲的softirq最近了,因为目前在软中断当中有主要的应用就是tasklet,而且在所有32个软中断中仅有限的几个软中断如下:
enum{
HI_SOFTIRQ=0,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
TASKLET_SOFTIRQ
};
struct softirq_action{
void (*action)(struct softirq_action *);
void *data;
};
static struct softirq_action softirq_vec[32] __cacheline_aligned; //软中断的中断向量表,实为数组.
[1]. 初始化软中断向量.
我们这里所要讲的,就是HI_SOFTIRQ / TASKLET_SOFTIRQ 两项,据我理解这两项根本在实现机制上一样的,之所以分开两个名字叫主要是为了将不同的功能分开,就类似于虽然同是软中断,但是各处所完成的功能不一样,所以分在两个软中断完成, 后面我们仅取其中用于执行时钟底半的任务队列HI_SOFTIRQ为例进行讲解, 而且我们不讲及多个CPU情况下的tasklet相关机制, 这两项软中断的实始化如下:
void __init softirq_init()
{
….
open_softirq(TASKLET_SOFTIRQ, tasklet_action, NULL);
open_softirq(HI_SOFTIRQ, tasklet_hi_action, NULL);
}
open_softirq下所做的事相当简单, 即往软中断向量中赋值, 相当于硬中断当中的request_irq挂硬件中断:
void open_softirq(int nr, void (*action)(struct softirq_action*), void *data)
{
softirq_vec[nr].data = data;
softirq_vec[nr].action = action;
}
[2]. 软中断中断服务程序
对于HI_SOFTIRQ , 相应的中断服务程序为tasklet_hi_action , 由上文所讲的初始化过程给出,这个函数目前完成的功能相当简单,它的任务就是遍历执行此中断所对应一个tasklet链表,
NR_CPUS= 1.
struct tasklet_head tasklet_hi_vec[NR_CPUS] __cacheline_aligned;
[3]. 往软中断对应的tasklet链表中加入新的tasklet, 加在尾部.
void __tasklet_hi_schedule(struct tasklet_struct *t)
{
…
t->next = tasklet_hi_vec[cpu].list;
tasklet_hi_vec[cpu].list = t;
cpu_raise_softirq(cpu, HI_SOFTIRQ);
…
}
最重要的一点是,在安装了新的tasklet后,还必须将软中断设置为激活,告诉系统有软中断须要执行了,下面一点即提到系统如何检测是否有软中断须要处理:
#define __cpu_raise_softirq(cpu, nr) do { softirq_pending(cpu) |= 1UL << (nr); } while (0)
[4]. 软中断所依赖的执行机制.
讲到最后还没有指出软中断是如何触发执行的,其实很简单:
在系统处理所有硬中断信号时,他们的入口是统一的,在这个入口函数当中除了执行do_IRQ()完成硬件中断的处理之外,还会执行do_softirq()来检测是否有软中断须要执行,所以软中断所依赖的是硬件中断机制;
另外还有一个专门处理软中断内核线程ksoftirqd(),这个线程处理软中断级别是比较低的,他是一个无限LOOP不停的检测是否有软中断须要处理,如果没有则进行任务调度.
在do_softirq()中有如下的判断,以决定是否有软中断须要执行,如果没有就直接退出,在[3]中提到的激活软中断时,要将相应软中断位置1, 软中断有32个,因此一个整型数即可以表示32个软中断,即可判断有什么样的软中断须要处理,代码如下:
pending = softirq_pending(cpu);
if (pending) {
}
….
do { //检测32个软中断位标志中是否有为1的…
if (pending & 1)
h->action(h);
h++;
pending >>= 1;
} while (pending);
[4]. 软中断所依赖的执行时期问题.
之所以将这个问题单独列开来讲,是因为他特别的重要,上面我已经讲过了软中断是依赖硬中断触发执行的,但是产生如下疑问:
是不是一有硬中断发生就会触发软中断的执行?
软中断的执行会不会影响到系统的性能?
会不会影响到硬中断的处理效率?也就是说会不会导致在处理软中断时而引起硬中断无法及时响应呢?
再看do_softirq的代码当中有如下判断:
if (in_interrupt())
return;
这个条件就是能否进行软中断处理的关键条件,因此由此也可以了解到软中断是一种优先级低于硬中断的软性机制,具体来看看这个判断条件是什么:
/*Are we in an interrupt context? Either doing bottom half
* or hardware interrupt processing?*/
#define in_interrupt() ({ const int __cpu = smp_processor_id();
(local_irq_count(__cpu) + local_bh_count(__cpu) != 0); })
/* softirq.h is sensitive to the offsets of these fields */
typedef struct {
unsigned int __softirq_pending;
unsigned int __local_irq_count;
unsigned int __local_bh_count;
unsigned int __syscall_count;
struct task_struct * __ksoftirqd_task; /* waitqueue is too large */
} ____cacheline_aligned irq_cpustat_t;
#define irq_enter(cpu,irq) (local_irq_count(cpu)++)
#define irq_exit(cpu,irq) (local_irq_count(cpu)--)
看到这里,不得不再多注意一个结构,那就是irq_cpustat_t, 先前我们讲是否有软中断产生的标志位,但没有提到__softirq_pending,这个变量就是记载32个软中断是否产生的标志,每一个软中断对应一个位; 在中断执行的do_softirq中有如下几个重要的动作,说明如下:
in_interrupt判断是否可以进行软中断处理,判断的条件就是没有没处在硬件中断环境中,而且还没有软中断正在执行(即不允许软中断嵌套),软中断的嵌套避免是通过local_bh_disable()/local_bh_enable()实现,至于带有bh,其意也即指softirq是中断底半(bh), 在处理硬件中断时,一进行即会调用irq_enter来表示已经进入硬件中断处理程序,处理完硬件中断后再调用irq_exit表示已经完成处理;
pending判断是否有软中断须要处理, 每个位用作当作一个软中断是否产生的标志.
清除所有软中断标志位,因为下面即将处理; 但清除之前先缓存起来, 因为下面还要使用这个变量一次.
在进入软中断处理后,会关闭bh功能的执行,执行完后才打开,这样在in_interrupt判断当中就会直接发现已经有bh在执行,不会再次进入bh执行了,这严格保证了bh执行的串行化.
打开硬件中断,让软中断在有硬件中断的环境下执行.
处理完软中断后关闭硬中断,再次检测是否有新的软中断产生,如果有的话,却只须立即处理本次软中断过程未发生过的软中断向量. 之所以会有新的软中断产生,那是因为软中断是在开硬件中断的情况下执行,硬件中断处理是可能又产生了新的软中断. 之所以只处理本次软中断未发生的软中断向量,依据我自己的理解,其目的是为了不加重软中断处理的负担而不马上处理,只是相应的唤醒一个wakeup_softirqd线程,这是专门处理软中断的,这样虽然延误了软中断的处理,但避免了在硬中断服务程序中拖延太长的时间.[关于软中断的处理在后绪版本变化也很大,可以进一步学习研究,如何使软中断不至影响中断处理效率]
软中断处理这个函数虽然不长,但是相当的关键,每一句代码都很重要,结合上面所说的几点,与源码交互起来理解才能根本理解软中断的设计机制:
asmlinkage void do_softirq()
{
int cpu = smp_processor_id();
__u32 pending;
unsigned long flags;
__u32 mask;
if (in_interrupt()) return;
local_irq_save(flags);
pending = softirq_pending(cpu);
if (pending) {
struct softirq_action *h;
mask = ~pending;
local_bh_disable();
restart:
/* Reset the pending bitmask before enabling irqs */
softirq_pending(cpu) = 0;
local_irq_enable();
h = softirq_vec;
do {
if (pending & 1)
h->action(h);
h++;
pending >>= 1;
} while (pending);
local_irq_disable();
pending = softirq_pending(cpu);
if (pending & mask) {
mask &= ~pending;
goto restart;
}
__local_bh_enable();
if (pending)
wakeup_softirqd(cpu);
}
local_irq_restore(flags);
}
}
四. TTY与串口的具体关联.
串口设备可以当作TTY终端来使用,这又使串口设备比一般的设备稍微复杂一些,因为他还必须与终端驱动关联起来,虽然这部分与TTY的关联已经是属于公用部分的代码,并不须要驱动编写者特别做些什么来进行支持,但对它与TTY的层次关联的了解有助于理解整个串口的数据流向.
串口要能够成为终端,必须客外加入终端注册及初始化的代码,这部分很简单,基本上所有的串口驱动都是固定的模式,并无什么修改,主要包括如下结构:
static struct console cs_amba_console = {
.name = "ttyBM",
.write = w83697uart_console_write,
.device = w83697uart_console_device,
.setup = w83697uart_console_setup,
.flags = CON_PRINTBUFFER,
.index = -1,
};
串口终端的注册通过下面的函数,将cs_amba_console注册成为终端, 这个函数调用路径是:
start_kernel()→console_init()→ep93xxuart_w83697_console_init()
void __init ep93xxuart_w83697_console_init(void)
终端会对应一种具体设备的driver, 相对于串口这个结构是uart_driver, 在驱动中我们已经提供了一个这样的结构. static struct uart_driver amba_reg, uart_register_driver会将它注册成为终端对应的driver, 因此真正串口与终端的关联就在此处建立.
函数: static int __init w83697uart_init(void)
描述: 调用uart_register_driver()完成串口与终端的关联,将串口注册成为一种TTY设备,在uart_register_driver()当中调用tty_register_driver()完成TTY设备注册; 其次是完成串口port口的注册,将静态描述的所有串口port(结构为struct uart_port)注册到uart_driver当中.
特别说明: 注册串口TTY设备时,由于历史的原因会注册两个TTY设备,一个是normal, 另一个是callout, 是两个设备来的, 在我们这里两者没有什么差别,请看源码中的注解:
.normal_name = "ttyBM",
.callout_name = "cuaam",
/*
* The callout device is just like the normal device except for
* the major number and the subtype code.
*/
函数: static void __exit w83697uart_exit(void)
描述: 卸截设备,卸截port口,因为我编译的驱动是与内核绑定在一起的,因此实际上根本不会调用此函数.