本节将分析数据结构——Linux 下用于管理多任务环境的机制。
一个 Linux 任务可以处于以下状态之一(根据 [include/linux.h])
______________ CPU Available ______________ | | ----------------> | | | TASK_RUNNING | | Real Running | |______________| <---------------- |______________| CPU Busy | /|\ Waiting for | | Resource Resource | | Available \|/ | ______________________ | | | TASK_INTERRUPTIBLE / | | TASK-UNINTERRUPTIBLE | |______________________| Main Multitasking Flow
每 10 毫秒(取决于 HZ 值)会产生一个 IRQ0,这有助于我们进行多任务环境。这个信号来自 PIC 8259(在 arch 386+ 中),它连接到时钟频率为 1.19318 MHz 的 PIT 8253。
_____ ______ ______ | CPU |<------| 8259 |------| 8253 | |_____| IRQ0 |______| |___/|\| |_____ CLK 1.193.180 MHz // From include/asm/param.h #ifndef HZ #define HZ 100 #endif // From include/asm/timex.h #define CLOCK_TICK_RATE 1193180 /* Underlying HZ */ // From include/linux/timex.h #define LATCH ((CLOCK_TICK_RATE + HZ/2) / HZ) /* For divider */ // From arch/i386/kernel/i8259.c outb_p(0x34,0x43); /* binary, mode 2, LSB/MSB, ch 0 */ outb_p(LATCH & 0xff , 0x40); /* LSB */ outb(LATCH >> 8 , 0x40); /* MSB */
因此,我们使用 LATCH = (1193180/HZ) = 11931.8 (当 HZ=100 时,默认值) 对 8253 (PIT,可编程间隔定时器) 进行编程。LATCH 表示频率除数因子。
LATCH = 11931.8 给 8253(在输出端)一个 1193180 / 11931.8 = 100 Hz 的频率,因此周期 = 10ms
因此,时间片 = 1/HZ。
在每个时间片中,我们临时中断当前进程的执行(不进行任务切换),并进行一些内务处理工作,之后我们将返回到我们之前的进程。
Linux Timer IRQ IRQ 0 [Timer] | \|/ |IRQ0x00_interrupt // wrapper IRQ handler |SAVE_ALL --- |do_IRQ | wrapper routines |handle_IRQ_event --- |handler() -> timer_interrupt // registered IRQ 0 handler |do_timer_interrupt |do_timer |jiffies++; |update_process_times |if (--counter <= 0) { // if time slice ended then |counter = 0; // reset counter |need_resched = 1; // prepare to reschedule |} |do_softirq |while (need_resched) { // if necessary |schedule // reschedule |handle_softirq |} |RESTORE_ALL
函数可以在以下位置找到
注释
描述
为了管理多任务,Linux(像每个其他 Unix 一样)使用一个 “counter”(计数器)变量来跟踪任务使用了多少 CPU 时间。因此,在每个 IRQ 0 上,计数器会递减(第 4 点),当它达到 0 时,我们需要切换任务来管理时间共享(第 4 点 “need_resched” 变量被设置为 1,然后,在第 5 点汇编例程控制 “need_resched” 并在需要时调用 “schedule” [kernel/sched.c])。
调度器是选择在给定时间必须执行哪个任务的代码片段。
任何时候你需要更改正在运行的任务,请选择一个候选者。以下是 “schedule [kernel/sched.c]” 函数。
|schedule |do_softirq // manages post-IRQ work |for each task |calculate counter |prepare_to__switch // does anything |switch_mm // change Memory context (change CR3 value) |switch_to (assembler) |SAVE ESP |RESTORE future_ESP |SAVE EIP |push future_EIP *** push parameter as we did a call |jmp __switch_to (it does some TSS work) |__switch_to() .. |ret *** ret from call using future_EIP in place of call address new_task
在经典的 Unix 中,当 IRQ 来自(设备)时,Unix 会进行 “task switching”(任务切换)以询问请求设备的任务。
为了提高性能,Linux 可以将非紧急工作推迟到以后,以便更好地管理高速事件。
自内核 1.x 以来,此功能由 “bottom half”(底半部,BH)管理。irq 处理程序 “marks”(标记)一个底半部,以便稍后在调度时间内执行。
在最新的内核中,有一个 “task queue”(任务队列),它比 BH 更具动态性,并且还有一个 “tasklet” 来管理多处理器环境。
BH 模式是
#define DECLARE_TASK_QUEUE(q) LIST_HEAD(q) #define LIST_HEAD(name) \ struct list_head name = LIST_HEAD_INIT(name) struct list_head { struct list_head *next, *prev; }; #define LIST_HEAD_INIT(name) { &(name), &(name) } ''DECLARE_TASK_QUEUE'' [include/linux/tqueue.h, include/linux/list.h]
“DECLARE_TASK_QUEUE(q)” 宏用于声明一个名为 “q” 的结构,该结构管理任务队列。
这是 “mark_bh” [include/linux/interrupt.h] 函数的 ICA 模式
|mark_bh(NUMBER) |tasklet_hi_schedule(bh_task_vec + NUMBER) |insert into tasklet_hi_vec |__cpu_raise_softirq(HI_SOFTIRQ) |soft_active |= (1 << HI_SOFTIRQ) ''mark_bh''[include/linux/interrupt.h]
例如,当 IRQ 处理程序想要 “postpone”(推迟)一些工作时,它会 “mark_bh(NUMBER)”,其中 NUMBER 是一个声明的 BH(见前一节)。
我们可以看到从 “do_IRQ” [arch/i386/kernel/irq.c] 函数的调用
|do_softirq |h->action(h)-> softirq_vec[TASKLET_SOFTIRQ]->action -> tasklet_action |tasklet_vec[0].list->func
“h->action(h);” 是先前排队的函数。
set_intr_gate
set_trap_gate
set_task_gate (未使用)。
(*interrupt)[NR_IRQS](void) = { IRQ0x00_interrupt, IRQ0x01_interrupt, ..}
NR_IRQS = 224 [kernel 2.4.2]
现在我们将了解 Linux 内核如何从一个任务切换到另一个任务。
在许多情况下需要任务切换,例如以下情况
TASK SWITCHING TRICK #define switch_to(prev,next,last) do { \ asm volatile("pushl %%esi\n\t" \ "pushl %%edi\n\t" \ "pushl %%ebp\n\t" \ "movl %%esp,%0\n\t" /* save ESP */ \ "movl %3,%%esp\n\t" /* restore ESP */ \ "movl $1f,%1\n\t" /* save EIP */ \ "pushl %4\n\t" /* restore EIP */ \ "jmp __switch_to\n" \ "1:\t" \ "popl %%ebp\n\t" \ "popl %%edi\n\t" \ "popl %%esi\n\t" \ :"=m" (prev->thread.esp),"=m" (prev->thread.eip), \ "=b" (last) \ :"m" (next->thread.esp),"m" (next->thread.eip), \ "a" (prev), "d" (next), \ "b" (prev)); \ } while (0)
技巧在这里
U S E R M O D E K E R N E L M O D E | | | | | | | | | | | | Timer | | | | | | | Normal | IRQ | | | | | | | Exec |------>|Timer_Int.| | | | | | | | | .. | | | | | | \|/ | |schedule()| | Task1 Ret| | | | | |_switch_to|<-- | Address | |__________| |__________| | | | | | | | |S | | Task1 Data/Stack Task1 Code | | |w | | | | T|i | | | | a|t | | | | | | | | s|c | | | | | | Timer | | k|h | | | | | Normal | IRQ | | |i | | | | | Exec |------>|Timer_Int.| |n | | | | | | | | .. | |g | | | | | \|/ | |schedule()| | | Task2 Ret| | | | | |_switch_to|<-- | Address | |__________| |__________| |__________| |__________| Task2 Data/Stack Task2 Code Kernel Code Kernel Data/Stack
Fork 用于创建另一个任务。我们从一个父任务开始,并将许多数据结构复制到子任务。
| | | .. | Task Parent | | | | | | | fork |---------->| CREATE | | | /| NEW | |_________| / | TASK | / | | --- / | | --- / | .. | / | | Task Child / | | / | fork |<-/ | | |_________| Fork SysCall
新创建的任务(“子任务”)几乎与父任务(“父任务”)相同,只有少数差异
|sys_fork |do_fork |alloc_task_struct |__get_free_pages |p->state = TASK_UNINTERRUPTIBLE |copy_flags |p->pid = get_pid |copy_files |copy_fs |copy_sighand |copy_mm // should manage CopyOnWrite (I part) |allocate_mm |mm_init |pgd_alloc -> get_pgd_fast |get_pgd_slow |dup_mmap |copy_page_range |ptep_set_wrprotect |clear_bit // set page to read-only |copy_segments // For LDT |copy_thread |childregs->eax = 0 |p->thread.esp = childregs // child fork returns 0 |p->thread.eip = ret_from_fork // child starts from fork exit |retval = p->pid // parent fork returns child pid |SET_LINKS // insertion of task into the list pointers |nr_threads++ // Global variable |wake_up_process(p) // Now we can wake up just created child |return retval fork ICA
为了实现 Linux 的写时复制
| Page | Fault | Exception | | -----------> |do_page_fault |handle_mm_fault |handle_pte_fault |do_wp_page |alloc_page // Allocate a new page |break_cow |copy_cow_page // Copy old page to new one |establish_pte // reconfig Page Table pointers |set_pte Page Fault ICA