閱讀704 返回首頁    go 阿裏雲 go 技術社區[雲棲]


Linux之時鍾中斷

from:深入分析Linux內核源碼(https://oss.org.cn/kernel-book/

 時鍾中斷的產生

     Linux的OS時鍾的物理產生原因是可編程定時/計數器產生的輸出脈衝,這個脈衝送入CPU,就可以引發一個中斷請求信號,我們就把它叫做時鍾中斷。

“時鍾中斷”是特別重要的一個中斷,因為整個操作係統的活動都受到它的激勵。係統利用時鍾中斷維持係統時間、促使環境的切換,以保證所有進程共享CPU;利用時鍾中斷進行記帳、監督係統工作以及確定未來的調度優先級等工作。可以說,“時鍾中斷”是整個操作係統的脈搏。

時鍾中斷的物理產生如圖所示:

                       
                                                              圖 8253和8259A的物理連接方式

操作係統對可編程定時/計數器進行有關初始化,然後定時/計數器就對輸入脈衝進行計數(分頻),產生的三個輸出脈衝Out0、Out1、Out2各有用途,很多接口書都介紹了這個問題,我們隻看Out0上的輸出脈衝,這個脈衝信號接到中斷控製器8259A_1的0號管腳,觸發一個周期性的中斷,我們就把這個中斷叫做時鍾中斷,時鍾中斷的周期,也就是脈衝信號的周期,我們叫做“滴答”或“時標”(tick)。從本質上說,時鍾中斷隻是一個周期性的信號,完全是硬件行為,該信號觸發CPU去執行一個中斷服務程序,但是為了方便,我們就把這個服務程序叫做時鍾中斷。

Linux實現時鍾中斷的全過程

1.可編程定時/計數器的初始化

IBM PC中使用的是8253或8254芯片。有關該芯片的詳細知識我們不再詳述,隻大體介紹以下它的組成和作用,如下表5.1所示:

 

表         8253/8254的組成及作用

名稱

端口地址

工作方式

產生的輸出脈衝的用途

計數器0

0x40

方式3

時鍾中斷,也叫係統時鍾

計數器1

0x41

方式2

動態存儲器刷新

計數器2

0x42

方式3

揚聲器發聲

控製寄存器

0x43

/

用於8253的初始化,接收控製字

 

計數器0的輸出就是圖5.3中的Out0,它的頻率由操作係統的設計者確定,Linux對8253的初始化程序段如下(在/arch/i386/kernel/i8259.c的init_IRQ()函數中):

 

set_intr_gate(ox20, interrupt[0]);

/*在IDT的第0x20個表項中插入一個中斷門。這個門中的段選擇符設置成內核代碼段的選擇符,偏移域設置成0號中斷處理程序的入口地址。*/

 

outb_p(0x34,0x43);     /* 寫計數器0的控製字:工作方式2*/

outb_p(LATCH & 0xff , 0x40);   /* 寫計數初值LSB  計數初值低位字節*/  

outb(LATCH >> 8 , 0x40);   /* 寫計數初值MSB 計數初值高位字節*/

 

LATCH(英文意思為:鎖存器,即其中鎖存了計數器0的初值)為計數器0的計數初值,在/include/linux/timex.h中定義如下:

 

#define CLOCK_TICK_RATE    1193180    /* 圖5.3中的輸入脈衝 */

#define LATCH  ((CLOCK_TICK_RATE + HZ/2) / HZ)  /* 計數器0的計數初值 */

 

CLOCK_TICK_RATE是整個8253的輸入脈衝,如圖5.3中所示為1.193180MHz,是近似為1MHz的方波信號,8253內部的三個計數器都對這個時鍾進行計數,進而產生不同的輸出信號,用於不同的用途。

HZ表示計數器0的頻率,也就是時鍾中斷或係統時鍾的頻率,在/include/asm/param.h中定義如下:

 

#define HZ 100

 

2.與時鍾中斷相關的函數

下麵我們看時鍾中斷觸發的服務程序,該程序代碼比較複雜,分布在不同的源文件中,主要包括如下函數:

時鍾中斷程序:timer_interrupt( );

中斷服務通用例程do_timer_interrupt();

時鍾函數:do_timer( );

中斷安裝程序:setup_irq( );

中斷返回函數:ret_from_intr( );

 

前三個函數的調用關係如下:

     timer_interrupt( )

                                      

                          do_timer_interrupt()

                                                          

                                                   do_timer( )

(1) timer_interrupt( )

    這個函數大約每10ms被調用一次,實際上, timer_interrupt( )函數是一個封裝例程,它真正做的事情並不多,但是,作為一個中斷程序,它必須在關中斷的情況下執行。如果隻考慮單處理機的情況,該函數主要語句就是調用do_timer_interrupt()函數。

 

(2) do_timer_interrupt()

do_timer_interrupt()函數有兩個主要任務,一個是調用do_timer( ),另一個是維持實時時鍾(RTC,每隔一定時間段要回寫),其實現代碼在/arch/i386/kernel/time.c中, 為了突出主題,筆者對以下函數作了改寫,以便於讀者理解:

 

static inline void do_timer_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

   do_timer(regs); /* 調用時鍾函數,將時鍾函數等同於時鍾中斷未嚐不可*/

    

   if(xtime.tv_sec > last_rtc_update + 660)

update_RTC();

 

 /*每隔11分鍾就更新RTC中的時間信息,以使OS時鍾和RTC時鍾保持同步,11分鍾即660秒,xtime.tv_sec的單位是秒,last_rtc_update記錄的是上次RTC更新時的值 */

                                                       

}

 

  其中,xtime是前麵所提到的timeval類型,這是一個全局變量。

 

(3) 時鍾函數do_timer() (在/kernel/sched.c中)

 

void do_timer(struct pt_regs * regs)

{

   (*(unsigned long *)&jiffies)++;  /*更新係統時間,這種寫法保證對jiffies

操作的原子性*/

 

 update_process_times();

    ++lost_ticks;

   if( ! user_mode ( regs ) )

       ++lost_ticks_system;

 

       mark_bh(TIMER_BH);           

   if (tq_timer)                    

       mark_bh(TQUEUE_BH);

}

 

其中,update_process_times()函數與進程調度有關,從函數的名子可以看出,它處理的是與當前進程與時間有關的變量,例如,要更新當前進程的時間片計數器counter,如果counter<=0,則要調用調度程序,要處理進程的所有定時器:實時、虛擬、概況,另外還要做一些統計工作。

與時間有關的事情很多,不能全都讓這個函數去完成,這是因為這個函數是在關中斷的情況下執行,必須處理完最重要的時間信息後退出,以處理其他事情。那麼,與時間相關的其他信息誰去處理,何時處理?這就是由第三章討論的後半部分去去處理。 上麵timer_interrupt()(包括它所調用的函數)所做的事情就是上半部分。

在該函數中還有兩個變量lost_ticks和lost_ticks_system,這是用來記錄timer_bh()執行前時鍾中斷發生的次數。因為時鍾中斷發生的頻率很高(每10ms一次),所以在timer_bh()執行之前,可能已經有時鍾中斷發生了,而timer_bh()要提供定時、記費等重要操作,所以為了保證時間計量的準確性,使用了這兩個變量。lost_ticks用來記錄timer_bh()執行前時鍾中斷發生的次數,如果時鍾中斷發生時當前進程運行於內核態,則lost_ticks_system用來記錄timer_bh()執行前在內核態發生時鍾中斷的次數,這樣可以對當前進程精確記費。

 

(4)中斷安裝程序

     從上麵的介紹可以看出,時鍾中斷與進程調度密不可分,因此,一旦開始有時鍾中斷就可能要進行調度,在係統進行初始化時,所做的大量工作之一就是對時鍾進行初始化,其函數time_init ()的代碼在/arch/i386/kernel/time.c中,對其簡寫如下:

 void __init time_init(void)

  {

xtime.tv_sec=get_cmos_time();

xtime.tv_usec=0;

setup_irq(0,&irq0);

}

  

   其中的get_cmos_time()函數就是把當時的實際時間從CMOS時鍾芯片讀入變量xtime中,時間精度為秒。而setup_irq(0,&irq0)就是時鍾中斷安裝函數,那麼irq0指的是什麼呢,它是一個結構類型irqaction,其定義及初值如下:

 

static struct irqaction irq0  = { timer_interrupt, SA_INTERRUPT, 0, "timer", NULL, NULL};

 

setup_irq(0, &irq0)的代碼在/arch/i386/kernel/irq.c中,其主要功能就是將中斷程序連入相應的中斷請求隊列,以等待中斷到來時相應的中斷程序被執行。

 

到現在為止,我們僅僅是把時鍾中斷程序掛入中斷請求隊列,什麼時候執行,怎樣執行,這是一個複雜的過程(參見第三章),為了讓讀者對時鍾中斷有一個完整的認識,我們忽略中間過程,而給出一個整體描述。我們將有關函數改寫如下,體現時鍾中斷的大意:

 

do_timer_interrupt( )          /*這是一個偽函數 */

{                                            

   SAVE_ALL                    /*保存處理機現場 */

   intr_count += 1;              /* 這段操作不允許被中斷 */

   timer_interrupt()             /* 調用時鍾中斷程序 */

   intr_count -= 1;              

   jmp ret_from_intr             /* 中斷返回函數 */

}

   

   其中,jmp ret_from_intr 是一段匯編代碼,也是一個較為複雜的過程,它最終要調用jmp ret_from_sys_call,即係統調用返回函數,而這個函數與進程的調度又密切相關,,因此,我們重點分析  jmp ret_from_sys_call。

   

3.係統調用返回函數:

  係統調用返回函數的源代碼在/arch/i386/kernel/entry.S中

  

ENTRY(ret_from_sys_call)

         cli                 # need_resched and signals atomic test

         cmpl $0,need_resched(%ebx)

         jne reschedule

         cmpl $0,sigpending(%ebx)

         jne signal_return

 restore_all:

         RESTORE_ALL

 

         ALIGN

 signal_return:

         sti              # we can get here from an interrupt handler

         testl $(VM_MASK),EFLAGS(%esp)

         movl %esp,%eax

         jne v86_signal_return

         xorl %edx,%edx

         call SYMBOL_NAME(do_signal)

         jmp restore_all

 

         ALIGN

        v86_signal_return:

         call SYMBOL_NAME(save_v86_state)

         movl %eax,%esp

         xorl %edx,%edx

         call SYMBOL_NAME(do_signal)

         jmp restore_all

  ….

 reschedule:

         call SYMBOL_NAME(schedule)    # test

        jmp ret_from_sys_call

 

這一段匯編代碼就是前麵我們所說的“從係統調用返回函數”ret_from_sys_call,它是從中斷、異常及係統調用返回時的通用接口。這段代碼主體就是ret_from_sys_call函數,其執行過程中要調用其它一些函數(實際上是一段代碼,不是真正的函數),在此我們列出相關的幾個函數:

(1)ret_from_sys_call:主體

(2)reschedule:檢測是否需要重新調度

(3)signal_return:處理當前進程接收到的信號

(4)v86_signal_return:處理虛擬86模式下當前進程接收到的信號

(5)RESTORE_ALL:我們把這個函數叫做徹底返回函數,因為執行該函數之後,就返回到當前進程的地址空間中去了。

可以看到ret_from_sys_call的主要作用有:

檢測調度標誌need_resched,決定是否要執行調度程序;處理當前進程的信號;恢複當前進程的環境使之繼續執行。

 

最後我們再次從總體上瀏覽一下時鍾中斷:

每個時鍾滴答,時鍾中斷得到執行。時鍾中斷執行的頻率很高:100次/秒,時鍾中斷的主要工作是處理和時間有關的所有信息、決定是否執行調度程序以及處理下半部分。和時間有關的所有信息包括係統時間、進程的時間片、延時、使用CPU的時間、各種定時器,進程更新後的時間片為進程調度提供依據,然後在時鍾中斷返回時決定是否要執行調度程序。下半部分處理程序是Linux提供的一種機製,它使一部分工作推遲執行。時鍾中斷要絕對保證維持係統時間的準確性,而下半部分這種機製的提供不但保證了這種準確性,還大幅提高了係統性能。



 

最後更新:2017-04-03 18:51:44

  上一篇:go 雙色點陣動態顯示
  下一篇:go Java、PHP、C、Ruby 語言相互吐槽的搞笑圖片