中断门初始化
核心代码:
#define _set_gate(gate_addr,type,dpl,addr) \
__asm__ ("movw %%dx,%%ax\n\t" \
"movw %0,%%dx\n\t" \
"movl %%eax,%1\n\t" \
"movl %%edx,%2" \
: \
: "i" ((short) (0x8000+(dpl<<13)+(type<<8))), \
"o" (*((char *) (gate_addr))), \
"o" (*(4+(char *) (gate_addr))), \
"d" ((char *) (addr)),"a" (0x00080000))
中断描述符的每一个中断门占8个字节。前四个字节描述中断处理方法类型,后四个字节描述Gate属性。
Gate属性中,第15位表示该gate属于中断门还是内陷门,1表示中断门,0表示内陷门。用16进制分别是0x8000和0x4000。第13、14位表示优先级别,一般为0(内核级别)和3(用户级别)。
上述代码中,输入为一个立即数和两个地址,以及edx和eax。
立即数是一个16位的数,将这个立即数赋值给gate_addr的第5、6个字节。addr赋值给gate_addr的低四字节,表示中断方法。这段函数的作用是初始化一个中断门或者内陷门。
typedef struct desc_struct {
unsigned long a,b;
} desc_table[256];
这段代码声明了一个结构体数组,名为desc_table,长度为256,其中每个元素都是一个结构体desc_struct。
中断初始化源码:
void trap_init(void)
{
int i;
set_trap_gate(0,÷_error);
set_trap_gate(1,&debug);
set_trap_gate(2,&nmi);
set_system_gate(3,&int3); /* int3-5 can be called from all */
set_system_gate(4,&overflow);
set_system_gate(5,&bounds);
set_trap_gate(6,&invalid_op);
set_trap_gate(7,&device_not_available);
set_trap_gate(8,&double_fault);
set_trap_gate(9,&coprocessor_segment_overrun);
set_trap_gate(10,&invalid_TSS);
set_trap_gate(11,&segment_not_present);
set_trap_gate(12,&stack_segment);
set_trap_gate(13,&general_protection);
set_trap_gate(14,&page_fault);
set_trap_gate(15,&reserved);
set_trap_gate(16,&coprocessor_error);
for (i=17;i<48;i++)
set_trap_gate(i,&reserved);
set_trap_gate(45,&irq13);
outb_p(inb_p(0x21)&0xfb,0x21);
outb(inb_p(0xA1)&0xdf,0xA1);
set_trap_gate(39,¶llel_interrupt);
}
- set_trap_gate():用于设置一个中断门(interrupt gate)或陷阱门(trap gate)。
- set_system_gate():用于设置一个系统门(system gate),这是用于系统调用的一种特殊类型的门。
- 每个中断门或系统门都由一个段选择符和一个偏移量组成,指向一个处理程序。因此,第二个参数是一个函数地址,这些函数指针被用作中断处理程序的地址。
中断设备子系统初始化
tty_init()是Linux内核中终端设备(tty)子系统的初始化函数之一,它用于初始化系统中的终端设备。该函数调用了两个函数,rs_init()和con_init(),分别用于初始化串行终端和控制台终端。
在Linux内核中,tty是指任何一种通过字符设备文件进行输入输出的设备,例如串行端口、控制台终端、伪终端(pty)等。tty子系统是Linux内核的一个重要组成部分,它负责处理终端设备的输入输出和控制,以及终端设备与应用程序之间的交互。
void rs_init(void)
{
set_intr_gate(0x24,rs1_interrupt);
set_intr_gate(0x23,rs2_interrupt);
init(tty_table[1].read_q.data);
init(tty_table[2].read_q.data);
outb(inb_p(0x21)&0xE7,0x21);
}
rs_init()函数做了以下几件事情:
- 调用set_intr_gate()函数,将0x24号和0x23号中断向量号对应的中断门(interrupt gate)设置为对应的串行端口1和串行端口2中断处理程序(rs1_interrupt和rs2_interrupt)的入口地址。
- 调用init()函数,初始化tty_table数组中编号为1和2的终端设备的读队列(read_q.data),为接收数据做好准备。
- 将0x21号中断控制器的相应位清零,以允许接收来自串行端口的中断信号。具体地,通过inb_p()函数读取0x21号端口的值,将其与0xE7按位与,再通过outb()函数将结果写回0x21号端口。
这段代码的作用是初始化串行端口的中断处理程序和读队列,并允许串行端口发出中断信号。
其中inb_p为:
#define inb_p(port) ({ \
unsigned char _v; \
__asm__ volatile ("inb %%dx,%%al\n" \
"\tjmp 1f\n" \
"1:\tjmp 1f\n" \
"1:":"=a" (_v):"d" (port)); \
_v; \
})
inb 是x86汇编指令,表示从指定端口读取一个字节(byte)的数据,并将其存储在 al 寄存器中。
“\tjmp 1f\n” \和"1:\tjmp 1f\n" \是一种延迟技巧,具体来说,inb 操作可能需要一些时间来完成,因此需要一种方式来确保操作已经完成。由于CPU执行速度非常快,如果使用循环来等待会导致CPU极度繁忙,因此需要使用一种技巧,即使用两个相同的标号 1: 来创建一个短小的延迟。
“=a” (_v) 是一个输出操作数,表示将 al 寄存器中的值返回,并将返回值保存到变量 _v 中;
:“d” (port) 是一个输入操作数,表示将变量 port 中的值加载到 dx 寄存器中。
而outb源码为:
#define outb(value,port) \
__asm__ ("outb %%al,%%dx"::"a" (value),"d" (port))
输出为空,输入:value的值给到ax寄存器,port给到dx寄存器。outb %%al,%%dx将 al 寄存器的值写入到 dx 端口中。
调度器初始化
void sched_init(void)
{
int i;
struct desc_struct * p;
if (sizeof(struct sigaction) != 16)
panic("Struct sigaction MUST be 16 bytes");
set_tss_desc(gdt+FIRST_TSS_ENTRY,&(init_task.task.tss));
set_ldt_desc(gdt+FIRST_LDT_ENTRY,&(init_task.task.ldt));
p = gdt+2+FIRST_TSS_ENTRY;
for(i=1;i<NR_TASKS;i++) {
task[i] = NULL;
p->a=p->b=0;
p++;
p->a=p->b=0;
p++;
}
/* Clear NT, so that we won't have troubles with that later on */
__asm__("pushfl ; andl $0xffffbfff,(%esp) ; popfl");
ltr(0);
lldt(0);
outb_p(0x36,0x43); /* binary, mode 3, LSB/MSB, ch 0 */
outb_p(LATCH & 0xff , 0x40); /* LSB */
outb(LATCH >> 8 , 0x40); /* MSB */
set_intr_gate(0x20,&timer_interrupt);
outb(inb_p(0x21)&~0x01,0x21);
set_system_gate(0x80,&system_call);
}
它设置了TSS和LDT段描述符,以及清除NT标志,以便后面不会出现问题。然后,它设置了时钟中断,并初始化LAPIC以便进行多处理器调度。最后,它设置了系统调用门。
在保护模式下,全局描述符表(Global Descriptor Table,简称GDT)是一个存储段描述符的数据结构,用于管理系统中所有的内存段。每个段描述符包括一个基地址和一个长度,用于指定内存段的起始地址和大小。操作系统和应用程序可以通过加载段选择子来访问和操作这些内存段。GDT中的每个段描述符都与一个唯一的选择子相关联。
任务状态段(Task State Segment,简称TSS)是一种特殊的段,用于存储处理器在任务切换时需要保存和恢复的状态信息。每个任务都有自己的TSS。在任务切换时,处理器会自动保存当前任务的状态到它所对应的TSS中,并加载新任务的TSS中保存的状态。因此,TSS是实现任务切换的关键。
局部描述符表(Local Descriptor Table,简称LDT)是一个存储段描述符的数据结构,类似于GDT。每个进程都有自己的LDT,用于存储其自身的局部段描述符。通过使用LDT,进程可以将自己的内存空间隔离开来,从而保护自己的内存空间不被其他进程访问。和GDT一样,LDT中的每个段描述符也与一个唯一的选择子相关联。
在Linux 0.11内核中,通过调用set_tss_desc()和set_ldt_desc()函数来设置TSS和LDT的描述符,并将它们写入GDT中。当需要进行任务切换或进程切换时,处理器会根据加载的TSS选择子或LDT选择子来访问相应的TSS或LDT中的段描述符。因此,TSS、LDT和GDT是保护模式下实现任务切换和内存隔离的关键数据结构。
在保护模式下,GDT中可以存储8192个8字节的段描述符。由于一个段描述符占8个字节,因此GDT的总大小为65536字节。在Linux 0.11内核中,GDT被定义为一个大小为64KB的数组,因此最多可以存储8192个段描述符。在实际应用中,大多数操作系统并不需要使用这么多的段描述符,因此一般只会在必要时使用GDT中的一部分段描述符。
tss、ldt、gdt的关系
在GDT中,每个段描述符都包括以下信息:
基地址(Base Address):指定内存段的起始地址。
段限长(Segment Limit):指定内存段的长度。
段类型(Segment Type):指定段的类型,如代码段、数据段、TSS段等。
特权级(Descriptor Privilege Level,DPL):指定该段可以被访问的特权级别。
系统标志(System Flag):指定段的一些特殊属性,如是否可执行、是否可写等。
选择子(Selector):用于访问该段,包括GDT选择子和LDT选择子。
TSS和LDT的描述符都包括基地址和段限长,而且在GDT中都被分配了一个唯一的选择子。当需要访问TSS或LDT时,处理器会使用相应的选择子从GDT中检索出相应的段描述符,并使用其中的基地址和段限长等信息来访问对应的内存段。
选择子的数据结构是怎样的
选择子是一种16位的数据结构,用于在保护模式下选择和访问全局描述符表(GDT)或局部描述符表(LDT)中的段描述符。选择子包含以下字段:
- 段选择子(Segment Selector):用于选择GDT或LDT中的一个段描述符,它是一个13位的偏移量,指向GDT或LDT中的一个段描述符的起始地址。
- 请求特权级(Requested Privilege Level,RPL):指定访问该段的请求特权级别。
- 局部/全局(Local/Global):用于区分是访问LDT还是GDT中的段描述符。当选择子中的位0设置为0时,表示该选择子用于访问GDT中的段描述符;当位0设置为1时,表示该选择子用于访问LDT中的段描述符。
在Linux 0.11内核中,用于访问TSS的选择子和LDT的选择子是通过在段选择子中设置特定的偏移量来区分的。具体来说:
- 访问TSS的选择子:TSS的段描述符在GDT中,其段选择子中偏移量的计算方法为FIRST_TSS_ENTRY << 3,其中FIRST_TSS_ENTRY表示TSS在GDT中的偏移量,<< 3表示偏移量需要乘以8(因为一个段描述符的长度为8字节)。
- 访问LDT的选择子:LDT的段描述符也在GDT中,其段选择子中偏移量的计算方法为FIRST_LDT_ENTRY << 3,其中FIRST_LDT_ENTRY表示LDT在GDT中的偏移量,<< 3同样表示偏移量需要乘以8。
void buffer_init(long buffer_end)
{
struct buffer_head * h = start_buffer;
void * b;
int i;
if (buffer_end == 1<<20)
b = (void *) (640*1024);
else
b = (void *) buffer_end;
while ( (b -= BLOCK_SIZE) >= ((void *) (h+1)) ) {
h->b_dev = 0;
h->b_dirt = 0;
h->b_count = 0;
h->b_lock = 0;
h->b_uptodate = 0;
h->b_wait = NULL;
h->b_next = NULL;
h->b_prev = NULL;
h->b_data = (char *) b;
h->b_prev_free = h-1;
h->b_next_free = h+1;
h++;
NR_BUFFERS++;
if (b == (void *) 0x100000)
b = (void *) 0xA0000;
}
h--;
free_list = start_buffer;
free_list->b_prev_free = h;
h->b_next_free = free_list;
for (i=0;i<NR_HASH;i++)
hash_table[i]=NULL;
}
缓冲区初始化的函数buffer_init,其主要目的是初始化一定数量的缓冲区,以便文件系统使用。
函数接受一个buffer_end参数,表示缓冲区的结束地址。函数首先初始化一个指向缓冲区头的指针h,并将b指针初始化为缓冲区结束地址。然后,使用while循环从高地址向低地址遍历缓冲区,为每个缓冲区头结构体buffer_head设置属性,包括设备号、脏标志、引用计数、锁标志、数据是否已更新、等待队列、指向下一个和上一个缓冲区头的指针、数据区的地址等。其中,NR_BUFFERS是全局变量,表示缓冲区的数量,遍历过程中每初始化一个缓冲区头,就将该变量加一。当b的值为0x100000时,将b的值重置为0xA0000,以保证缓冲区不会超出内存空间。
初始化完成后,将最后一个缓冲区头结构体h指向的缓冲区作为起始的空闲缓冲区,将该缓冲区头结构体指针赋给全局变量free_list。然后,将空闲缓冲区按照地址从低到高的顺序链接成一个双向链表,每个缓冲区头结构体包含指向前一个和后一个空闲缓冲区头结构体的指针。最后,初始化哈希表hash_table,将每个槽初始化为NULL。
磁盘初始化代码
在介绍磁盘初始化代码前,有必要解释磁盘相关概念的意义。
- 扇区 、磁头、柱面、磁道图解
盘片
一个磁盘(如一个 1T 的机械硬盘)由多个盘片(如下图中的 0 号盘片)叠加而成。
盘片的表面涂有磁性物质,这些磁性物质用来记录二进制数据。因为正反两面都可涂上磁性物质,故一个盘片可能会有两个盘面。
磁道、扇区
每个盘片被划分为一个个磁道,每个磁道又划分为一个个扇区。如下图:
其中,最内侧磁道上的扇区面积最小,因此数据密度最大。
柱面
每个盘面对应一个磁头。所有的磁头都是连在同一个磁臂上的,因此所有磁头只能“共进退”。所有盘面中相对位置相同的磁道组成柱面。如下图,
磁盘的物理地址
由上,可用(柱面号,盘面号,扇区号)来定位任意一个“磁盘块”。
我们经常提到文件数据存放在外存中的几号块(逻辑地址),这个块号就可以转换成(柱面号,盘面号,扇区号)的地址形式。
可根据该地址读取一个“块”,操作如下:
① 根据“柱面号”移动磁臂,让磁头指向指定柱面;
② 激活指定盘面对应的磁头;
③ 磁盘旋转的过程中,指定的扇区会从磁头下面划过,这样就完成了对指定扇区的读/写。
- 硬件初始化流程
这段代码实现的是硬盘I/O操作的请求处理函数do_hd_request(),它在Linux内核中用于处理硬盘设备的读写请求。
函数首先获取当前请求的设备号和起始扇区号,然后进行一系列的计算来将扇区号转换为柱面号、磁头号和扇区号。如果需要重置硬盘,函数会调用reset_hd()函数进行重置,并返回。如果需要重新校准硬盘,函数会调用hd_out()函数进行校准,并返回。
如果当前请求是写请求,函数会调用hd_out()函数向硬盘控制器发送写命令,并等待硬盘控制器就绪。如果硬盘控制器就绪,函数会调用port_write()函数将数据写入硬盘,否则会调用bad_rw_intr()函数进行错误处理。
如果当前请求是读请求,函数会调用hd_out()函数向硬盘控制器发送读命令,并等待硬盘控制器就绪。如果硬盘控制器就绪,函数会调用read_intr()函数进行读取操作,读取到的数据会存储在CURRENT->buffer中。
如果当前请求是未知类型的命令,函数会调用panic()函数进行内核崩溃处理。
最后,函数会调用end_request()函数结束当前请求,并跳转到repeat标签处,等待下一个请求的到来。
其中汇编可能比较难懂:
__asm__("divl %4":"=a" (block),"=d" (sec):"0" (block),"1" (0),
"r" (hd_info[dev].sect)); //用除法得到起始扇区所在的磁道和扇区号
__asm__("divl %4":"=a" (cyl),"=d" (head):"0" (block),"1" (0),
"r" (hd_info[dev].head)); //cyl 代表磁道所在的柱面号, head代表磁头号 用除法得到起始扇区所在的柱面和磁头号
- inb、outb、inb_p、outb_p作用
inb和outb是汇编指令,用于从指定端口读取数据和向指定端口写入数据,它们对应的C函数是inb()和outb()。在x86架构中,端口分为8位和16位两种,因此指令名中的b和w分别表示操作数据宽度为8位和16位。
inb_p()和outb_p()是Linux内核中的函数,它们与inb()和outb()功能相同,但它们使用了特殊的I/O延迟方式。在内核代码中,inb()和outb()函数可能会被编译器优化掉,导致CPU不会真正执行这些操作,而是直接跳过。而inb_p()和outb_p()函数则会使用一些特殊的指令或方法,例如插入rep nop(空循环),来增加I/O操作的延迟,以确保I/O操作能够真正被执行。
在磁盘驱动程序中,inb_p()和outb_p()函数通常用于与磁盘控制器进行数据交换。由于磁盘控制器是一个独立的硬件设备,与CPU之间的数据交换需要经过I/O端口,因此需要使用这些函数来确保数据能够正确传输
//例如
outb_p(hd_info[drive].ctl,HD_CMD);
hd_info[drive].ctl表示控制字的值,HD_CMD表示将命令控制字发送给控制器的端口号。
在处理磁盘请求方法中,hd_out方法是比较重要的:
static void hd_out(unsigned int drive,unsigned int nsect,unsigned int sect,
unsigned int head,unsigned int cyl,unsigned int cmd,
void (*intr_addr)(void))
{
register int port asm("dx");
if (drive>1 || head>15)
panic("Trying to write bad sector");
if (!controller_ready())
panic("HD controller not ready");
do_hd = intr_addr;
outb_p(hd_info[drive].ctl,HD_CMD);
port=HD_DATA;
outb_p(hd_info[drive].wpcom>>2,++port);
outb_p(nsect,++port);
outb_p(sect,++port);
outb_p(cyl,++port);
outb_p(cyl>>8,++port);
outb_p(0xA0|(drive<<4)|head,++port);
outb(cmd,++port);
}
这段代码实现的是向硬盘控制器发送命令的函数hd_out(),它在Linux内核中用于向硬盘控制器发送读写命令以及其他命令。
函数首先对输入的参数进行检查,判断磁头号和驱动器号是否合法。然后调用controller_ready()函数检查硬盘控制器是否就绪,如果硬盘控制器没有准备好,函数会调用panic()函数进行内核崩溃处理。
接下来,函数会设置全局变量do_hd为intr_addr,这个变量保存了硬盘中断处理函数的地址。
函数会向硬盘控制器发送一系列的命令和参数,这些命令和参数用于控制硬盘的读写操作。具体来说,函数会依次向数据寄存器HD_DATA写入硬盘起始位置、扇区数、扇区号、柱面号低8位、柱面号高8位、驱动器号和命令码。
最后,函数会返回,等待硬盘控制器完成操作并调用硬盘中断处理函数do_hd。
其中比较重要的是controller_ready方法,代码如下:
static int controller_ready(void)
{
int retries=10000;
while (--retries && (inb_p(HD_STATUS)&0xc0)!=0x40);
return (retries);
}
这段代码实现的是检查硬盘控制器是否就绪的函数controller_ready(),它在Linux内核中用于检查硬盘控制器的状态是否正常。
函数使用了一个循环来等待硬盘控制器就绪,循环中的语句会不断地读取硬盘状态寄存器HD_STATUS,并进行位运算,判断硬盘控制器是否处于就绪状态。如果硬盘控制器就绪,函数会返回1,否则会继续循环等待,直到超时或硬盘控制器就绪为止。
具体来说,函数将硬盘状态寄存器HD_STATUS的高2位屏蔽掉,然后与0x40进行比较,如果相等,说明硬盘控制器处于就绪状态,函数返回1。否则,函数会继续循环等待,直到超时或硬盘控制器就绪为止。
需要注意的是,这里的retries变量控制了最大的等待次数,如果超过了这个次数仍然没有检测到硬盘控制器就绪,函数会返回0,表示失败。
下面用一个写磁盘的请求例子结束磁盘初始化:
if (CURRENT->cmd == WRITE) {
hd_out(dev,nsect,sec,head,cyl,WIN_WRITE,&write_intr);
for(i=0 ; i<3000 && !(r=inb_p(HD_STATUS)&DRQ_STAT) ; i++)
/* nothing */ ;
if (!r) {
bad_rw_intr();
goto repeat;
}
port_write(HD_DATA,CURRENT->buffer,256);
判断当前的命令是否为WRITE,如果是,则继续执行,否则不执行。
调用hd_out函数,将磁盘驱动器的设备号、扇区数、扇区号、磁头号、柱面号以及WIN_WRITE等参数传递给该函数,同时将写操作的中断处理函数write_intr作为参数传递给该函数。
进入一个循环,循环次数为3000。在循环中,首先读取HD_STATUS寄存器的值,并将其与DRQ_STAT按位与运算。如果结果为0,则表示磁盘驱动器没有准备好接收数据,就继续等待;否则,表示磁盘驱动器已准备好接收数据,跳出循环。
判断循环结束后的结果r是否为0。如果是0,则表示磁盘驱动器没有准备好接收数据,此时调用bad_rw_intr函数进行错误处理,并跳转到repeat标签处重新执行该操作;否则,表示磁盘驱动器已准备好接收数据,继续执行下一步操作。
hd_out函数中的write_intr参数是一个中断处理函数的地址,它的作用是在磁盘驱动器完成写入数据操作后,能够向操作系统发送中断信号,以便操作系统能够及时处理磁盘驱动器的响应结果。
软盘初始化
void do_fd_request(void)
{
unsigned int block;
seek = 0; //是否进行磁道寻道操作
if (reset) { //是否复位软盘驱动器
reset_floppy();
return;
}
if (recalibrate) { //是否需要重新校准软盘驱动器
recalibrate_floppy();
return;
}
INIT_REQUEST; //初始化磁盘请求
floppy = (MINOR(CURRENT->dev)>>2) + floppy_type; //获取软盘设备的类型
if (current_drive != CURRENT_DEV)
seek = 1; //这句代码的作用是判断当前软盘驱动器的编号 current_drive 是否与当前进程正在使用的软盘设备的编号 CURRENT_DEV 相同。如果不同则需要寻道
current_drive = CURRENT_DEV;
block = CURRENT->sector; //将当前进程正在访问的软盘设备的当前扇区号赋值给变量 block
if (block+2 > floppy->size) {
end_request(0);
goto repeat;
}
sector = block % floppy->sect; //floppy->sect:软盘每个磁道上的扇区数;sector:当前请求需要访问的数据所在的扇区在当前磁道上的偏移量;
block /= floppy->sect;
head = block % floppy->head; //floppy->head:软盘的磁头数;head:当前请求需要访问的数据所在的磁头号;
track = block / floppy->head; //track:当前请求需要访问的数据所在的磁道号;
seek_track = track << floppy->stretch; //seek_track:需要寻道到的磁道号,即当前请求需要访问的数据所在的磁道号,乘以一个伸缩因子(stretch)得到的值。
if (seek_track != current_track)
seek = 1;
sector++;
if (CURRENT->cmd == READ)
command = FD_READ;
else if (CURRENT->cmd == WRITE)
command = FD_WRITE;
else
panic("do_fd_request: unknown command");
add_timer(ticks_to_floppy_on(current_drive),&floppy_on_interrupt);
}
这段是处理软盘驱动器请求的函数。函数会根据请求类型进行不同的操作,比如复位软盘驱动器、校准软盘驱动器、读写软盘等。函数会根据当前请求的设备号和扇区号计算出磁头、磁道和扇区号,然后设置相应的命令并添加定时器来触发软盘驱动器的读写操作。
根据当前请求的设备号,可以计算出对应的软盘驱动器类型(floppy_type)。然后根据当前请求的扇区号,计算出磁头、磁道和扇区号。
具体的计算方法如下:
根据当前请求的扇区号和软盘驱动器的扇区数(floppy->sect),可以计算出所在的磁道和磁头:
sector = block % floppy->sect; // 当前请求的扇区号
head = block / floppy->sect % floppy->head; // 磁头号
track = block / (floppy->head * floppy->sect); // 磁道号
设置定时器代码:
void add_timer(long jiffies, void (*fn)(void))
{
struct timer_list * p;
if (!fn)
return;
cli();
if (jiffies <= 0)
(fn)();
else {
for (p = timer_list ; p < timer_list + TIME_REQUESTS ; p++)
if (!p->fn)
break;
if (p >= timer_list + TIME_REQUESTS)
panic("No more time requests free");
p->fn = fn;
p->jiffies = jiffies;
p->next = next_timer;
next_timer = p;
while (p->next && p->next->jiffies < p->jiffies) {
p->jiffies -= p->next->jiffies;
fn = p->fn;
p->fn = p->next->fn;
p->next->fn = fn;
jiffies = p->jiffies;
p->jiffies = p->next->jiffies;
p->next->jiffies = jiffies;
p = p->next;
}
}
sti();
}
这段代码是用来添加定时器的函数。函数会根据传入的延迟时间(jiffies)和回调函数(fn),创建一个定时器,并将其添加到定时器链表中。
具体的实现方法是,首先判断传入的回调函数是否为空,如果为空则直接返回。否则,遍历定时器链表,找到一个空闲的定时器(即fn为NULL的定时器)。如果没有空闲的定时器,则会触发内核崩溃(panic)。
然后,将回调函数(fn)、延迟时间(jiffies)和下一个定时器(next_timer)存储到定时器结构体中,并将该定时器插入到定时器链表中。插入时,会根据定时器的延迟时间,将该定时器插入到链表中正确的位置。具体的插入方法是,从链表头开始,找到第一个延迟时间比当前定时器长的定时器,然后将当前定时器插入到该定时器之前。
最后,函数会开启中断(sti()),释放CPU控制权,让其他任务继续执行。