Linux內核進程調度以及定時器實現機制
http://blog.csdn.net/joshua_yu/archive/2006/02/02/591038.aspx
【摘要】本文簡單介紹了任務的各種狀態和PCB的結構,分析了幾種任務調度策略,詳解了schedule,并分析了如何進行進程上下文切換;隨后分析了2.6內核如何優化了任務調度算法;最后介紹了內核定時器的實現機制和系統調用的實現過程。
【關鍵詞】進程控制塊PCB,RR,FIFO,內核調度算法,任務切換,內核定時,timer,軟中斷softirq,系統調用
一、2.6版以前內核進程調度機制簡介
Linux的進程管理由進程控制塊、進程調度、中斷處理、任務隊列、定時器、bottom half隊列、系統調用、進程通信等等部分組成。
進程調用分為實時進程調度和非實時進程調度兩種。前者調度時,可以采用基于動態優先級的輪轉法(RR),也可以采用先進先出算法(FIFO)。后者調度時,一律采用基于動態優先級的輪轉法。某個進程采用何種調度算法由該進程的進程控制塊中的某些屬性決定,沒有專門的系統用來處理關于進程調度的相關事宜。Linux的進程調度由schedule( )函數負責,任何進程,當它從系統調用返回時,都會轉入schedule( ),而中斷處理函數完成它們的響應任務以后,也會進入schedule( )。
1、進程控制塊數據結構
Linux系統的進程控制塊用數據結構task_struct表示,這個數據結構占用1680個字節,具體的內容不在這里介紹,詳細內容見《Linux內核2.4版源代碼分析大全》第二頁。
進程的狀態主要包括如下幾個:
TASK_RUNNING 正在運行或在就緒隊列run-queue中準備運行的進程,實際參與進程調度。
TASK_INTERRUPTIBLE 處于等待隊列中的進程,待資源有效時喚醒,也可由其它進程通過信號或定時中斷喚醒后進入就緒隊列run-queue。
TASK_UNINTERRUPTIBLE 處于等待隊列的進程,待資源有效時喚醒,不可由其它進程通過信號或者定時中斷喚醒。
TASK_ZOMBIE 表示進程結束但尚未消亡的一種狀態(僵死),此時,進程已經結束運行并且已經釋放了大部分資源,但是尚未釋放進程控制塊。
TASK_STOPPED 進程暫停,通過其它進程的信號才能喚醒。
所有進程(以PCB形式)組成一個雙向列表。next_task和prev_task就是鏈表的前后向指針。鏈表的頭尾都是init_task(init進程)。不過進程還要根據其進程ID號插入到一個hash表當中,目的是加快進程搜索速度。
2、進程調度
Linux進程調度由schedule( )執行,其任務是在run-queue隊列中選出一個就緒進程。
每個進程都有一個調度策略,在它的task_struct中規定(policy屬性),或為SCHED_RR,SCHED_FIFO,或為SCHED_OTHER。前兩種為實時進程調度策略,后一種為普通進程調度策略。
用戶進程由do_fork( )函數創建,它也是fork系統調用的執行者。do_fork( )創建一個新的進程,繼承父進程的現有資源,初始化進程時鐘、信號、時間等數據。完成子進程的初始化后,父進程將它掛到就緒隊列,返回子進程的pid。進程創建時的狀態為TASK_UNINTERRUPTIBLE,在do_fork( )結束前被父進程喚醒后,變為TASK_RUNNING。處于TASK_RUNNING狀態的進程被移到就緒隊列中,當適當的時候由schedule( )按CPU調度算法選中,獲得CPU。
如果進程采用輪轉法,當時間片到時(10ms的整數倍),由時鐘中斷觸發timer_interrupt( )函數引起新一輪的調度,把當前進程掛到就緒隊列的尾部。獲得CPU而正在運行的進程若申請不到某個資源,則調用sleep_on( )或interruptible_sleep_on( )睡眠,并進入就緒隊列尾。狀態為TASK_INTERRUPTIBLE的睡眠進程當它申請的資源有效時被喚醒,也可以由信號或者定時中斷喚醒,喚醒以后進程狀態變為TASK_RUNNING,并進入就緒隊列。
首先介紹一下2.6版以前的的調度算法的主要思想,下面的schedule( )函數是內核2.4.23中摘錄的:
asmlinkage void schedule(void)
{
struct schedule_data * sched_data;
struct task_struct *prev, *next, *p;
struct list_head *tmp;
int this_cpu, c;
spin_lock_prefetch(&runqueue_lock);
BUG_ON(!current->active_mm);
need_resched_back:
/*記錄當前進程和處理此進程的CPU號*/
prev = current;
this_cpu = prev->processor;
/*判斷是否處在中斷當中,這里不允許在中斷處理當中調用sechedule( )*/
if (unlikely(in_interrupt( ))) {
printk("Scheduling in interrupt\n");
BUG( );
}
release_kernel_lock(prev, this_cpu);
/*'sched_data' 是受到保護的,每個CPU只能運行一個進程。*/
sched_data = & aligned_data[this_cpu].schedule_data;
spin_lock_irq(&runqueue_lock);
/*如果當前進程的調度策略是輪轉RR,那么需要判斷當前進程的時間片是否已經用完,如果已經用完,則重新計算時間片值,然后將該進程掛接到就緒隊列run-queue的最后*/
if (unlikely(prev->policy == SCHED_RR))
if (!prev->counter) {
prev->counter = NICE_TO_TICKS(prev->nice);
move_last_runqueue(prev);
}
/*假如進程為TASK_INTERRUPTTIBLE狀態,則將其狀態置為TASK_RUNNING。如是其它狀態,則將該進程轉為睡眠狀態,從運行隊列中刪除。(已不具備運行的條件) */
switch (prev->state) {
case TASK_INTERRUPTIBLE:
if (signal_pending(prev)) {
prev->state = TASK_RUNNING;
break;
}
default:
del_from_runqueue(prev);
case TASK_RUNNING:;
}
/*當前進程不需要重新調度*/
prev->need_resched = 0;
/*下面是一般的進程調度過程*/
repeat_schedule:
next = idle_task(this_cpu);
c = -1000;
/*遍歷進程就緒隊列,如果該進程能夠進行調度(對于SMP來說就是判斷當前CPU未被占用能夠執行這個進程,對于非SMP系統則為1),則計算該進程的優先級,如果優先級大于當前進程,則next指針指向新的進程,循環直到找到優先級最大的那個進程*/
list_for_each(tmp, &runqueue_head) {
p = list_entry(tmp, struct task_struct, run_list);
if (can_schedule(p, this_cpu)) {
int weight = goodness(p, this_cpu, prev->active_mm);
if (weight > c)
c = weight, next = p;
}
}
/* 判斷是否需要重新計算每個進程的時間片,判斷的依據是所有正準備進行調度的進程時間片耗盡,這時,就需要對就緒隊列中的每一個進程都重新計算時間片,然后返回前面的調度過程,重新在就緒隊列當中查找優先級最高的進程執行調度。 */
if (unlikely(!c)) {
struct task_struct *p;
spin_unlock_irq(&runqueue_lock);
read_lock(&tasklist_lock);
for_each_task(p)
p->counter = (p->counter >> 1) + NICE_TO_TICKS(p->nice);
read_unlock(&tasklist_lock);
spin_lock_irq(&runqueue_lock);
goto repeat_schedule;
}
/*CPU私有調度數據中記錄當前進程的指針,并且將當前進程與CPU綁定,如果待調度進程與前面一個進程屬于同一個進程,則不需要調度,直接返回。*/
sched_data->curr = next;
task_set_cpu(next, this_cpu);
spin_unlock_irq(&runqueue_lock);
if (unlikely(prev == next)) {
/* We won't go through the normal tail, so do this by hand */
prev->policy &= ~SCHED_YIELD;
goto same_process;
}
/*全局統計進程上下文切換次數*/
kstat.context_swtch++;
/*如果后進程的mm為0 (未分配頁),則檢查是否被在被激活的頁里(active_mm),否則換頁。令后進程記錄前進程激活頁的信息,將前進程的active_mm中的mm_count值加一。將cpu_tlbstate[cpu].state改為 TLBSTATE_LAZY(采用lazy模式) 如果后進程的mm不為0(已分配頁),但尚未激活,換頁。切換mm(switch_mm)。 如果前進程的mm 為0(已失效) ,將其激活記錄置空,將mm結構引用數減一,刪除該頁。 */
prepare_to_switch( );
{
struct mm_struct *mm = next->mm;
struct mm_struct *oldmm = prev->active_mm;
if (!mm) {
BUG_ON(next->active_mm);
next->active_mm = oldmm;
atomic_inc(&oldmm->mm_count);
enter_lazy_tlb(oldmm, next, this_cpu);
} else {
BUG_ON(next->active_mm != mm);
switch_mm(oldmm, mm, next, this_cpu);
}
if (!prev->mm) {
prev->active_mm = NULL;
mmdrop(oldmm);
}
}
/*切換到后進程,調度過程結束*/
switch_to(prev, next, prev);
__schedule_tail(prev);
same_process:
reacquire_kernel_lock(current);
if (current->need_resched)
goto need_resched_back;
return;
}
3、進程上下文切換
首先進程切換需要做什么?它做的事只是保留正在運行進程的"環境",并把將要運行的進程的"環境"加載上來,這個環境也叫上下文。它包括各個進程"公用"的東西,比如寄存器。
下一個問題,舊的進程環境保存在那,新的進程環境從那來,在i386上,有個tss段,是專用來保存進程運行環境的。在Linux來說,在結構task_struct中有個類型為struct thread_struct的成員叫tss,如下:
struct task_struct {
。。。
/* tss for this task */
struct thread_struct tss;
。。。
};
它是專用來存放進程環境的,這個結構體因CPU而異,你看它就能知道有那些寄存器是需要保存的了。 最后的問題就是切換了,雖然在i386上CPU可以自動根據tss去進行上下文的切換,但是Linux的程序員們更愿意自己做它,原因是這樣能得到更有效的控制,而且作者說這和硬件切換的速度差不多,這可是真的夠偉大的。
好了,現在來看源碼,進程切換是使用switch_to這個宏來做的,當進入時prev即是現在運行的進程,next是接下來要切換到的進程,
#define switch_to(prev,next,last) do { \
asm volatile(
"pushl %%esi\n\t" \
"pushl %%edi\n\t" \
"pushl %%ebp\n\t" \
// 首先它切換堆棧指針,prev->tss.esp = %esp;%esp = next->tss.esp,這以后的堆棧已經是next的堆棧了。
"movl %%esp,%0\n\t" /* save ESP */ \
"movl %3,%%esp\n\t" /* restore ESP */ \
// 然后使進程prev的指針保存為標號為1的那一個指針,這樣下次進程prev可以運行時,它第一個執行的就是pop指令。
"movl $1f,%1\n\t" /* save EIP */ \
// 把進程next保存的指針推進堆棧中,這句作用是,從__switch_to返回時,下一個要執行的指令將會是這個指針所指向的指令了。
"pushl %4\n\t" /* restore EIP */ \
// 使用jump跳到__switch_to函數的結果是:調用switch_to函數但不象call那樣要壓棧,但是ret返回時,仍是要彈出堆棧的,也就是上條指令中推進去的指令指針。這樣,堆棧和指令都換了,進程也就被"切換"了。
"jmp __switch_to\n" \
// 由于上面所說的原因,__switch_to返回后并不會執行下面的語句,要執行到這,只有等進程prev重新被調度了。
"1:\t" \
"popl %%ebp\n\t" \
"popl %%edi\n\t" \
"popl %%esi\n\t" \
:"=m" (prev->tss.esp),"=m" (prev->tss.eip), \
"=b" (last) \
:"m" (next->tss.esp),"m" (next->tss.eip), \
"a" (prev), "d" (next), \
"b" (prev)); \
} while (0)
最后是__switch_to函數,它雖然是c形式,但內容還都是嵌入匯編。
// 這句可能需要你去記起前面第二章中所描述的gdt表的結構了,它的作用是把進程next的tss描述符的type中的第二位清0,這位表示這個描述符是不是當前正在用的描述符,作者說如果不清0就把它load進tss段寄存器的話,系統會報異常(我可沒試過)。
gdt_table[next->tss.tr >> 3].b &= 0xfffffdff;
// 把進程next的tss段load到tss段存器中。
asm volatile("ltr %0": :"g" (*(unsigned short *)&next->tss.tr));
// 保存進程prev的fs和gs段寄存器
asm volatile("movl %%fs,%0":"=m" (*(int *)&prev->tss.fs));
asm volatile("movl %%gs,%0":"=m" (*(int *)&prev->tss.gs));
然后下面就是load進程next的ldt,頁表,fs,gs,debug寄存器。
因為Linux一般并不使用ldt,所以它們一般會指向一個共同的空的ldt段描述符,這樣就可能不需要切換ldt了,如果進程next和prev是共享內存的話,那么頁表的轉換也就不必要了(這一般發生在clone時)。
二、2.6版內核對進程調度的優化
1、新調度算法簡介
2.6版本的Linux內核使用了新的調度器算法,稱為O(1)算法,它在高負載的情況下執行得非常出色,并在有多個處理器時能夠很好地擴展。 2.4版本的調度器中,時間片重算算法要求在所有的進程都用盡它們的時間片后,新時間片才會被重新計算。在一個多處理器系統中,當進程用完它們的時間片后不得不等待重算,以得到新的時間片,從而導致大部分處理器處于空閑狀態,影響SMP的效率。此外,當空閑處理器開始執行那些時間片尚未用盡的、處于等待狀態的進程時,會導致進程開始在處理器之間“跳躍”。當一個高優先級進程或交互式進程發生跳躍時,整個系統的性能就會受到影響。
新調度器解決上述問題的方法是,基于每個CPU來分布時間片,并取消全局同步和重算循環。調度器使用了兩個優先級數組,即活動數組和過期數組,可以通過指針來訪問它們。活動數組中包含所有映射到某個CPU且時間片尚未用盡的任務。過期數組中包含時間片已經用盡的所有任務的有序列表。如果所有活動任務的時間片都已用盡,那么指向這兩個數組的指針互換,包含準備運行任務的過期數組成為活動數組,而空的活動數組成為包含過期任務的新數組。數組的索引存儲在一個64位的位圖中,所以很容易找到最高優先級的任務。
新調度器的主要優點包括:
◆ SMP效率 如果有工作需要完成,所有處理器都會工作。
◆ 等待進程 沒有進程需要長時間地等待處理器,也沒有進程會無端地占用大量的CPU時間。
◆ SMP進程映射 進程只映射到一個CPU,而且不會在CPU之間跳躍。
◆ 優先級 非重要任務的優先級低,反之亦然。
◆ 負載平衡 調度器會降低那些超出處理器負載能力的進程的優先級。
◆ 交互性能 即使在高負載的情況下,系統花費很長時間來響應鼠標點擊或鍵盤輸入的情況也不會再發生。
2.6版內核中,進程調度經過重新編寫,調度程序不需每次都掃描所有的任務,而是在一個任務變成就緒狀態時將其放到一個名為“當前”的隊列中。當進程調度程序運行時,只選擇隊列中最有利的任務來執行。這樣,調度可以在一個恒定的時間里完成。當任務執行時,它會得到一個時間段,或者在其轉到另一線程之前得到一段時間的處理器使用權。當時間段用完后,任務會被轉移到另一個名為“過期”的隊列中。在該隊列中,任務會根據其優先級進行排序。
從某種意義上說,所有位于“當前”隊列的任務都將被執行,并被轉移到“過期”隊列中。當這種事情發生時,隊列就會進行切換,原來的“過期”隊列成為“當前”隊列,而空的“當前”隊列則變成“過期”隊列。由于在新的“當前”隊列中,任務已經被排列好,調度程序現在使用簡單的隊列算法,即總是取當前隊列的第一個任務進行執行。這個新過程要比老過程快得多。
2、2.6版新調度算法分析
下面的schedule( )函數摘錄自2.6.6版內核源碼:
asmlinkage void __sched schedule(void)
{
long *switch_count;
task_t *prev, *next;
runqueue_t *rq;
prio_array_t *array;
struct list_head *queue;
unsigned long long now;
unsigned long run_time;
int idx;
if (likely(!(current->state & (TASK_DEAD | TASK_ZOMBIE)))) {
if (unlikely(in_atomic( ))) {
printk(KERN_ERR "bad: scheduling while atomic!\n");
dump_stack( );
}
}
need_resched:
preempt_disable( );
prev = current;
/*獲取相應CPU的進程就緒隊列,每一個CPU都會有一個進程就緒等待隊列,每個等待隊列包括兩個優先級數組,活動數組和過期數組,兩個數組可以進行輪換。CPU相關等待隊列run-queue的數據結構定義如下:
struct runqueue {
spinlock_t lock; /*隊列自旋鎖*/
unsigned long long nr_switches;/*進程上下文切換次數計數器*/
unsigned long nr_running, expired_timestamp, nr_uninterruptible,
timestamp_last_tick;
task_t *curr, *idle; /*標記當前CPU正在執行的任務及空閑任務的PCB指針*/
struct mm_struct *prev_mm; /*內存數據結構*/
prio_array_t *active, *expired, arrays[2]; /*優先級活動和過期數組,活動數組用來標記當前時間片未用完并且處于等待調度狀態的進程列表,而過期數組用來標記當前時間片消耗完畢,無法繼續運行的進程列表*/
int best_expired_prio, prev_cpu_load[NR_CPUS];
task_t *migration_thread;
struct list_head migration_queue;
atomic_t nr_iowait;
}*/
rq = this_rq( );
release_kernel_lock(prev);
now = sched_clock( );
if (likely(now - prev->timestamp < NS_MAX_SLEEP_AVG))
run_time = now - prev->timestamp;
else
run_time = NS_MAX_SLEEP_AVG;
/*
* Tasks with interactive credits get charged less run_time
* at high sleep_avg to delay them losing their interactive
* status
*/
if (HIGH_CREDIT(prev))
run_time /= (CURRENT_BONUS(prev) ? : 1);
spin_lock_irq(&rq->lock);
/*
* if entering off of a kernel preemption go straight
* to picking the next task.
*/
switch_count = &prev->nivcsw;
if (prev->state && !(preempt_count( ) & PREEMPT_ACTIVE)) {
switch_count = &prev->nvcsw;
if (unlikely((prev->state & TASK_INTERRUPTIBLE) &&
unlikely(signal_pending(prev))))
prev->state = TASK_RUNNING;
else
deactivate_task(prev, rq);
}
/*如果當前就緒隊列中沒有正在運行的任務,則進行進程上下文切換,到空閑進程*/
if (unlikely(!rq->nr_running)) {
#ifdef CONFIG_SMP
load_balance(rq, 1, cpu_to_node_mask(smp_processor_id( )));
#endif
if (!rq->nr_running) {
next = rq->idle;
rq->expired_timestamp = 0;
goto switch_tasks;
}
}
/*獲取當前活動數組指針,判斷處于活動狀態的進程數目,如果活動數組當中已經沒有任何活動的進程,則需要將活動數組與過期數組進行對換,這樣就可以很快的開始新一輪的調度,這里array指針總是指向活動數組。*/
array = rq->active;
if (unlikely(!array->nr_active)) {
/*
* Switch the active and expired arrays.
*/
rq->active = rq->expired;
rq->expired = array;
array = rq->active;
rq->expired_timestamp = 0;
rq->best_expired_prio = MAX_PRIO;
}
/*由于調度器事先將活動數組的索引存放在了一個64位的bitmap位圖結構當中,排序的依據是根據每個進程的優先級排列的,所以當我們需要查找當前活動數組中優先級最高的進程的索引時,僅僅需要從位圖中找到最前面的一個索引值就可以了,而不需要象2.4內核那樣每次遍歷就緒隊列,計算每個進程的優先級然后才能確定應該選擇哪個進程作為下一個調度的對象。
查找到當前隊列中優先級最高的進程索引,然后在列表中找到相應的進程。*/
idx = sched_find_first_bit(array->bitmap);
queue = array->queue + idx;
next = list_entry(queue->next, task_t, run_list);
/*一個進程的優先級是從0到MAX_PRIO-1(MAX_PRIO為140),實時進程優先級是從0到MAX_RT_PRIO-1(MAX_RT_PRIO為100),SCHED_NORMAL任務的優先級在MAX_RT_PRIO到MAX_PRIO-1之間,優先級值越小表明優先級越大。
在這里首先判斷當前需要調度的進程是否為實時進程,如果不是看是不是處于活動狀態,如果是,根據其消耗的時間片,重新計算優先級。*/
if (!rt_task(next) && next->activated > 0) {
unsigned long long delta = now - next->timestamp;
if (next->activated == 1)
delta = delta * (ON_RUNQUEUE_WEIGHT * 128 / 100) / 128;
array = next->array;
dequeue_task(next, array);
recalc_task_prio(next, next->timestamp + delta);
enqueue_task(next, array);
}
next->activated = 0;
/*開始進行進程切換,關于內核如何進行上下文切換這里就不看了*/
switch_tasks:
reacquire_kernel_lock(current);
preempt_enable_no_resched( );
if (test_thread_flag(TIF_NEED_RESCHED))
goto need_resched;
}
3、2.6版新調度算法流程圖
三、內核中斷及定時器實現分析
定時器是Linux提供的一種定時服務的機制。它在某個特定的時間喚醒某個進程,來做一些工作。Linux初始化時,init_IRQ( )函數設定8253的定時周期為10ms(一個tick值)。同樣,在初始化時,time_init( )用setup_irq( )設置時間中斷向量irq0,中斷服務程序為timer_interrupt。
在2.4版內核及較早的版本當中,定時器的中斷處理采用底半部機制,底半處理函數的注冊在start_kernel( )函數中調用sechd_init( ),在這個函數中又調用init_bh(TIMER_BH, timer_bh)注冊了定時器的底半處理函數。然后系統才調用time_init( )來注冊定時器的中斷向量和中斷處理函數。
在中斷處理函數timer_interrupt( )中,主要是調用do_timer( )函數完成工作。do_timer( )函數的主要功能就是調用mark_bh( )產生軟中斷,隨后處理器會在合適的時候調用定時器底半處理函數timer_bh( )。在timer_bh( )中,實現了更新定時器的功能。2.4.23版的do_timer( )函數代碼如下(經過簡略):
void do_timer(struct pt_regs *regs)
{
(*(unsigned long *)&jiffies)++;
update_process_times(user_mode(regs));
mark_bh(TIMER_BH);
}
而在內核2.6版本以后,定時器中斷處理采用了軟中斷機制而不是底半機制。時鐘中斷處理函數仍然為timer_interrup( )-> do_timer_interrupt( )-> do_timer_interrupt_hook( )-> do_timer( )。不過do_timer( )函數的實現有所不同:
void do_timer(struct pt_regs *regs)
{
jiffies_64++;
update_process_times(user_mode(regs));
update_times( );
}
兩者所調用的函數基本相同,但是2.4.23版內核與2.6.6內核中,對于update_process_times( )函數的實現不同:
void update_process_times(int user_tick)
{
struct task_struct *p = current;
int cpu = smp_processor_id( ), system = user_tick ^ 1;
update_one_process(p, user_tick, system, cpu);
run_local_timers( );
scheduler_tick(user_tick, system);
}
update_process_times( )調用run_local_timers( )引起TIMER_SOFTIRQ定時器軟中斷,處理器在隨后的合適的時機運行軟中斷處理函數run_timer_softirq,這個函數是在init_timers( )函數中注冊的:
void __init init_timers(void)
{
timer_cpu_notify(&timers_nb, (unsigned long)CPU_UP_PREPARE,
(void *)(long)smp_processor_id( ));
register_cpu_notifier(&timers_nb);
open_softirq(TIMER_SOFTIRQ, run_timer_softirq, NULL);
}
事實上軟中斷處理函數run_timer_softirq( )并沒有做什么工作,主要的任務還是通過調用__run_timers( )函數完成的,這個函數相當于2.4.23內核當中的run_timer_list( )函數的功能。
static inline void __run_timers(tvec_base_t *base)
{
struct timer_list *timer;
spin_lock_irq(&base->lock);
/*這里進入定時器處理循環,利用系統全局jiffies與定時器基準jiffies進行對比,如果前者大,則表明有某些定時器需要進行處理了,否則表示所有的定時器都沒有超時*/
while (time_after_eq(jiffies, base->timer_jiffies)) {
struct list_head work_list = LIST_HEAD_INIT(work_list);
struct list_head *head = &work_list;
int index = base->timer_jiffies & TVR_MASK;
/*
在時間列表數據結構當中查找是否存在需要進行超時處理的定時器,時間列表的數據結構定義如下:
typedef struct tvec_s {
struct list_head vec[TVN_SIZE];
} tvec_t;
typedef struct tvec_root_s {
struct list_head vec[TVR_SIZE];
} tvec_root_t;
struct tvec_t_base_s {
spinlock_t lock;
unsigned long timer_jiffies;
struct timer_list *running_timer;
tvec_root_t tv1;
tvec_t tv2;
tvec_t tv3;
tvec_t tv4;
tvec_t tv5;
} ____cacheline_aligned_in_smp;
*/
if (!index &&
(!cascade(base, &base->tv2, INDEX(0))) &&
(!cascade(base, &base->tv3, INDEX(1))) &&
!cascade(base, &base->tv4, INDEX(2)))
cascade(base, &base->tv5, INDEX(3));
++base->timer_jiffies;
list_splice_init(base->tv1.vec + index, &work_list);
repeat:
/*如果當前找到的時間數組對應的列表不為空,則表明該列表上串連的所有定時器都已經超時,循環調用每個定時器的處理函數,并將其從列表中刪除,直到列表為空為止。*/
if (!list_empty(head)) {
void (*fn)(unsigned long);
unsigned long data;
timer = list_entry(head->next,struct timer_list,entry);
fn = timer->function;
data = timer->data;
list_del(&timer->entry);
set_running_timer(base, timer);
smp_wmb( );
timer->base = NULL;
spin_unlock_irq(&base->lock);
fn(data);
spin_lock_irq(&base->lock);
goto repeat;
}
}
set_running_timer(base, NULL);
spin_unlock_irq(&base->lock);
}
四、系統調用的實現過程
Linux系統調用的形式與POSIX兼容,也是一套C語言函數名的集合,如fork( ),read( )等等共221個。系統調用是通過INT 0x80軟中斷調用進入內核,然后根據系統調用號分門別類的進行服務。
系統調用號:文件include/asm-i386/unistd.h為每一個系統調用規定了唯一的編號,這個編號與真正的響應函數之間的關系是利用系統調用號為數組的下標,可以在sys_call_table(系統調用表數組)中查找對應的響應函數的sys_name的入口地址。
系統調用表:系統調用表sys_call_table是一組宏定義,將系統調用的編號和相應的內核系統調用處理函數的入口地址綁定。
內核宏定義syscallN( )用于系統調用的格式轉換和參數的傳遞。其中N取0~6之間的任意整數。參數個數為N的系統調用由syscallN負責格式轉換和參數傳遞。
對系統調用的初始化,即是對INT 0x80的初始化。系統啟動時,匯編子程序setup_idt準備了256項的idt表。設置0x80號軟中斷服務程序system_call,這個就是所有系統調用的總入口。
當進程需要進行系統調用時,必須以C語言函數的形式寫一句系統調用命令。該命令如果已經在某個頭文件中由相應的syscallN( )展開,則用戶程序必須包含該頭文件。當進程執行到系統調用命令時,實際上執行的是syscallN( )展開的函數。系統調用的參數由寄存器傳遞,然后執行INT 0x80中斷,以內核態進入入口地址system_call。
從system_call入口的匯編程序的主要功能是:
l 保存寄存器當前值;
l 檢驗是否為合法的系統調用;
l 根據系統調用表sys_call_table和EAX持有的系統調用號找出并轉入系統調用響應函數;
l 從該響應函數返回后,讓EAX寄存器保存函數返回值,跳轉至ret_from_sys_call(arch/i386/kernel/entry.S)。
以ret_from_sys_call入口的匯編程序段在Linux進程管理中起著十分重要的作用。所有系統調用結束前,以及大部分中斷服務程序返回前,都會跳轉至此入口地址。反過來,該段程序也不僅僅為系統調用服務,它還要處理中斷嵌套、bottom half隊列、CPU調度、信號等事務。
參考資料:
《Linux內核源碼》
《Linux內核2.4版源代碼分析大全》