Linux時鐘實現和管理(Linux Kernel development 3rd)

  • 簡介

    • 時間間隔

      • 這個概念在內核中非常重要。大量多的延時函數都是依賴於時間。

      • 週期性函數

        • 進程調度
        • 屏幕刷新
        • 延時硬盤讀寫
        • 系統從開機到現在運行了多久
        • 當前的日期
      • 上面列舉的都是用到週期的
    • 過去了多久,時間在系統中該怎麼衡量核心問題。

    • 相對時間

      • 從某一刻起後多久開始做某事。

      五秒後發射就是相對於現在一段時間。

    • 絕對時間

      日期,經歷了多久,往往是一個大的時間。

  • 週期任務和延遲任務

    • 週期任務

      • 依賴於系統時鐘,系統時鐘是一個可編程硬件。每隔固定時間就發起中斷。
      • 內核知道每秒多少次中斷,根據間隔和中斷次數,可以用來衡量時間。
    • 系統時鐘是核心

      • 這是一個硬件。
      • 一個可編程硬件,即可以配置,可以有內核控制。
      • 用來計量時間流逝。
      • 隔一段時間發起一次中斷,中斷捕獲就更新時間。然後執行對應的函數。
    • 延遲任務

      • 一般是事件,時間發生多久後執行某個響應函數。

      • 主要依賴於動態定時器

    • 動態定時器

      • 從開始計時的那一刻開始,多久後執行某個任務。

      • 這也是後面的主講。
      • 案例

      軟盤驅動在一定時間得不到響應就關閉。

  • 時間衡量

    • 依賴硬件

      系統時鐘

    • 時鐘頻率

      可配置

    • 轉換

      1s = 時鐘週期 * 時鐘頻率
      時鐘週期 = 1s / 時鐘頻率
      總的時間 = 時鐘週期 * 次數
      
    • 根據換算方程可以看到時鐘週期和過去時間

  • 動態定時器

    • 定時器

    用來倒計時的,倒計時多久執行某個任務。

    這個也是主要的核心中的核心。

  • 內核管理和衡量時間

    • 衡量時間

      藉助硬件系統時鐘的週期性中斷和次數來衡量。

      根據配置的頻率,按照對應週期觸發中斷。這個中斷由特殊的中斷處理器處理。

      • 查看中斷
        cat /proc/interrupts
        
      • 系統時間和已經開機時間也是藉助中斷次數來衡量。
    • 日期

      • 內核需要根據系統時鐘來更新系統日期信息。
      • 日期對用戶程序非常重要。
    • 系統運行時間

      從成功引導到現在過了多久。

      這個信息對內核和用戶程序都很重要。
    • 相對時間

      兩個時間戳之間做差就是相對時間。

  • 時鐘中斷典型應用

    • 更細系統運行時間

    • 更新系統日期信息

    • 多處理器用於多處理器之間處理任務的負載平衡。

    • 處理動態定時器及其事件

    • 更新資源的使用情況和處理器時間統計

    處理器時間指的是某個進程在這個處理器上執行了多久,是否應該讓出資源,讓下一個任務執行。

    <span style = "color:red">部分任務在每次觸發中斷的時候執行,有的則是觸發n次中斷後才執行。</span>
  • 時鐘頻率HZ

    • 配置

      • 時鐘中斷的頻率在系統引導模塊被定義,這個值是一個宏,編譯的時候已經確定。

        /*
         *  arch/arm/include/asm/param.h
         *
         *  Copyright (C) 1995-1999 Russell King
         *
         * This program is free software; you can redistribute it and/or modify
         * it under the terms of the GNU General Public License version 2 as
         * published by the Free Software Foundation.
         */
        #ifndef __ASM_PARAM_H
        #define __ASM_PARAM_H
        
        #ifdef __KERNEL__
        # define HZ		CONFIG_HZ	/* Internal kernel timer frequency */
        # define USER_HZ	100		/* User interfaces are in "ticks" */
        # define CLOCKS_PER_SEC	(USER_HZ)	/* like times() */
        #else
        # define HZ		100
        #endif
        
        #define EXEC_PAGESIZE	4096
        
        #ifndef NOGROUP
        #define NOGROUP         (-1)
        #endif
        
        /* max length of hostname */
        #define MAXHOSTNAMELEN  64
        
        #endif
        
      • 頻率一般來說是不一樣的,和架構,甚至和硬件也不一樣。
    • 換算關係

      時鐘週期 = 1 / 時鐘頻率
      時間週期是變量
      時鐘週期 =  F(時鐘頻率) = 1 / 時鐘頻率
      
      • 一般來說是100,當然也可以配置爲其他的值。
    • 重要性

      • 影響整個系統的執行效率。
      • 影響整個系統的執行交互性和流暢度。
    • 理想頻率

      • Linuxi386版本,時鐘中斷都是用的100,但是在開發2.5系列的時候,頻率提高到了1000,這個提升在當時是有爭議的.

      • 現在的頻率雖然也是100hz了,但是後來增加了配置選項,允許用戶自己設置頻率。
      • 時鐘頻率對整個系統的影響巨大,所以修改之前需要想好。
      • 增大帶來的一些影響。

      • 時鐘中斷處理函數執行得更頻繁。

        中斷函數要幹很多的事情。

      • 時鐘頻率變大,中段處理器的時間精確度更高,依賴於時鐘的定時器時間執行的時間越精確。

      • 定時器相關的事件的精準度也提升。

      • 精確

      • 100hz的時鐘頻率最小精度爲10ms,而1000hz的最小精度爲1ms
      • 雖然說內核可以提供1ms的中斷,但是整個系統的執行效果並不是說比100hz的就好很多。
      • 精準

      • 定時器在任意時刻啓動,執行的誤差範圍在半個時鐘週期內。也就是說,執行超時了,但是想要執行任務需要等時鐘中斷函數來處理,時鐘中斷函數的執行需要等到時鐘中斷觸發的時候才能執行。
      • 但是時鐘中斷是有周期的,就先等着,等着到了時鐘中斷來了再執行。
      • 所以這種就會有差不多半個週期的誤差,這個誤差再頻率高的時候就越小,頻率低的時候越大。
    • 高時鐘頻率的好處

      • 內核定時器執行得更加精確精準。

      • poll and select這兩個依賴於定時器事件的系統調用執行的效率更高。

      • 對於資源的統計,系統執行事件,也更加的精準。

      • 進程的佔用處理器的執行時長調度更加準確。

      • 較大提升的有

        • poll select的超時,這種非阻塞的IO,很依賴於超時的準確。前面說的超時等待有誤差,對於這種頻繁調用系統接口的誤差則是更大。
        • 另一個是進程調度,每一次時鐘中斷的時候就會將那些正在使用處理器的進程的市場減小,當爲0,且當有nedd_sched標識置爲一的時候就進行進程切換。這個和定時器也有些類似,比如一個進程擁有2ms的執行時長,但是最小精度是10ms,那麼有8ms等待時間,這個就是誤差。但是這種誤差是都有的誤差,都可以接收的。而且在有些地方這種誤差是良性的。
    • 高頻率的壞處

      • 高頻率的中斷也就意味着頻繁的執行中斷處理函數,也就代表着高的開銷。處理器主要是爲用戶進程服務的,這個開銷太大,導致用戶進程佔用的時間變少。而且還會導致處理器的緩存命中率降低。頻繁的執行中斷,會引發用戶熱數據被不斷的置換出三級緩存,導致了執行效率降低。而且耗電。

      • 後面的操作系統和設備的升級1000hz的開銷也不是不可以接收。

    • 無頻率的操作系統

      • 時鐘週期中斷真的需要嗎?
      • 雖然一直以來都是使用的時鐘中斷的操作系統。但是也是有不需要時鐘中斷的操作系統的。而且Linux也支持這種系統。編譯的時候配置。
      • Linux編譯的時候配置宏CONFIG_HZ,那麼內核就是動態的執行中斷。也就不是以前的定期的執行,而是想什麼時候執行就什麼時候執行。比如3ms 後 50ms都是可能的。
      • 這種系統不但減少了開支,而且還省電,尤其是在空轉的時候,大家都休息。
      • 標準的時鐘中斷系統需要爲時鐘中斷服務,哪怕是在空轉的時候,而這種系統則不需要,在空轉的時候就屏蔽時鐘中斷。
  • 時鐘計數Jiffies

    • 系統運行時間

      • 系統開機了有多久,這個信息也是很多用戶進程和內核都需要的一個信息。

      • 通過計算

      時鐘週期 = 1 / 時鐘頻率
      開機時長 = 時鐘週期 * 中斷次數
             = 中斷次數 / 時鐘頻率
      
      • 在引導系統的時候賦予初值0,每次時鐘中斷觸發,就會加一。

      • 顯示使用中,這個變量比較複雜,這個變量的初始值會設置爲一個非0值,很大,讓這個變量自動溢出,以此來檢測bug.當要求實際的值得時候,就減去初始值就可以了。

    • 定義

      /*
      * The 64-bit value is not atomic - you MUST NOT read it
      * without sampling the sequence number in xtime_lock.
      * get_jiffies_64() will do this for you as appropriate.
      */
      extern u64 __jiffy_data jiffies_64;
      extern unsigned long volatile __jiffy_data jiffies;
      
      #if (BITS_PER_LONG < 64)
      // 如果32位
      u64 get_jiffies_64(void);
      #else
      static inline u64 get_jiffies_64(void)
      {
      	return (u64)jiffies;
      }
      #endif
      
    • 與時間之間的轉換

      unsigned long time_stamp = jiffies; /* now */
      unsigned long next_tick = jiffies + 1; /* one tick from now */
      unsigned long later = jiffies + 5*HZ; /* five seconds from now */
      unsigned long fraction = jiffies + HZ / 10; /* a tenth of a second from now */
      
    • 內部的表達方式

      • 溢出問題

        32位100hz系統溢出需要497天,321000hz系統溢出需要49.7天,64就永不溢出。

        linuxunsigned long32位是4字節,64位是8字節,windows不論32還是64都是4字節。
      • 鏈接實體

        //linux-2.6.39\arch\x86\kernel\vmlinux.lds.S
        #ifdef CONFIG_X86_32
        OUTPUT_ARCH(i386)
        ENTRY(phys_startup_32)
        jiffies = jiffies_64;
        #else
        OUTPUT_ARCH(i386:x86-64)
        ENTRY(phys_startup_64)
        jiffies_64 = jiffies;
        #endif
        
      • 原子操作即數據總線攜帶的數據的位數小於等於總線帶寬。

        • 32位的系統,操作64位的數據,則需要用到順序鎖輔助。
        #if (BITS_PER_LONG < 64)
        u64 get_jiffies_64(void)
        {
        	unsigned long seq;
        	u64 ret;
        
        	do {
        		seq = read_seqbegin(&xtime_lock);
        		ret = jiffies_64;
        	} while (read_seqretry(&xtime_lock, seq));
        	return ret;
        }
        EXPORT_SYMBOL(get_jiffies_64);
        #endif
        
        • 上面用到了順序鎖。
      • 32位的時候衡量過去了多久用的是32位的jiffies,而且用的還是64jiffies_64的低32位值,時間管理則用jiffies_64,64的時候就是一個東西了。

      • jiffies值溢出迴繞

        #define time_after(a,b)		\
        	(typecheck(unsigned long, a) && \
        	 typecheck(unsigned long, b) && \
        	 ((long)(b) - (long)(a) < 0))
        #define time_before(a,b)	time_after(b,a)
        
        #define time_after_eq(a,b)	\
        	(typecheck(unsigned long, a) && \
        	 typecheck(unsigned long, b) && \
        	 ((long)(a) - (long)(b) >= 0))
        #define time_before_eq(a,b)	time_after_eq(b,a)
        

        由於溢出可能會帶來問題,所以用這種方式來進性比較。

    • 用戶應用於頻率

      • 2.6以前用戶應用也會使用到頻率。這個值由內核暴露給用戶空間。這些接口永久的不變,即返回固定值。那如果你改變了hz,但是這個函數沒有改變,就導致了用戶讀取錯誤。比如1000hz2h,用戶讀取到的就是20h.

      • 爲了防止這個問題,內核就進性了等比例收縮jiffies.定義一個用戶空間USER_HZ.然後定義了一個HZ用於內核。通過算式轉換爲等價值。

        clock_t jiffies_to_clock_t(long x)
        {
        #if (TICK_NSEC % (NSEC_PER_SEC / USER_HZ)) == 0
        # if HZ < USER_HZ
        	return x * (USER_HZ / HZ);
        # else
        	return x / (HZ / USER_HZ);
        # endif
        #else
        	return div_u64((u64)x * TICK_NSEC, NSEC_PER_SEC / USER_HZ);
        #endif
        }
        unsigned long clock_t_to_jiffies(unsigned long x)
        {
        #if (HZ % USER_HZ)==0
        	if (x >= ~0UL / (HZ / USER_HZ))
        		return ~0UL;
        	return x * (HZ / USER_HZ);
        #else
        	/* Don't worry about loss of precision here .. */
        	if (x >= ~0UL / HZ * USER_HZ)
        		return ~0UL;
        
        	/* .. but do try to contain it here */
        	return div_u64((u64)x * HZ, USER_HZ);
        #endif
        }
        
    • 硬件時鐘和定時器

      • 處理器提供了兩種時鐘

        • 系統可編程時鐘。
        • <span style="color:red">實時時鐘</span>
      • 目的差不多,實現差異比較大。

    • <span style="color:red">實時時鐘</span>

      • 爲系統提供並存儲時間的一種穩定設備。

      • 即使系統關機,主板上的鈕釦電池也可以爲這個硬件供電,以保證正常運行。

      • 電腦中RTCCMOS被繼承到一起,這個簡單的鈕釦電池保證RTC的執行和BIOS的數據被保存。

        • RTC實時時鐘,主板上的硬件

        • CMOS存儲BIOSRTC值的地方,一種半導體硬件。

      • 系統引導的時候就會讀取這個RTC值,主要是用來初始化日期,日期數據存放在xtime中。一般內核就讀取以一次。

    • <span style="color:red">系統可編程時鐘</span>

      • 主要的中斷硬件。
      • 不管什麼架構,目的都是相同的

        爲系統提供一個週期性的中斷。不斷的提醒系統時間到了,該做什麼了。

      • 時鐘中斷的兩種實現

        • 通過電子時鐘即按照給定頻率震盪的方式。
        • 另一種就是一個倒計時值,值到了0就發起中斷。
      • PIT : programmable interrupt timer

        • 可編程中斷時鐘,在DOS之後幾乎所有的硬件都支持。

        • 引導內核的時候就已經配置好硬件。然後硬件就按照這個頻率進性週期性的中斷。

  • 時鐘中斷處理函數

    • 時鐘中斷的兩部分

    • 依賴於處理器架構部分。

    • 不依賴處理器架構部分。

    • 依賴於處理器架構部分

    • 這個是主體部分,中斷觸發了,先執行這個部分,具體的工作根據架構不同而不同。
    • 大部分的架構工作都是相似的。

      • 獲取xtime_lock這個鎖,用於安全訪問變量jiffies_64,以及日期變量xtime.
      • 有必要還會通知系統時鐘或者重置系統時鐘。
      • 週期性的保存實時時鐘的值,即真正的日期值,用於校正。
      • 調用與架構無關部分的代碼tick_periodic,即通用部分。
    • 不依賴處理器架構部分tick_periodic

    • jiffies_64安全加一。
    • 更新資源使用信息,比如當前運行進程的用戶時間執行了多久,以及消耗的系統資源。
    • 執行已經超時了的動態定時器的回調函數。
    • 調用scheduler_tick,即進程調度。
    • 更新日期時間,xtime
    • 更新負載均衡
    /*
    * Periodic tick
    */
    static void tick_periodic(int cpu)
    {
    	if (tick_do_timer_cpu == cpu) {
    		write_seqlock(&xtime_lock);
    
    		/* Keep track of the next tick event */
    		tick_next_period = ktime_add(tick_next_period, tick_period);
    
    		do_timer(1);
    		write_sequnlock(&xtime_lock);
    	}
    
    	update_process_times(user_mode(get_irq_regs()));
    	profile_tick(CPU_PROFILING);
    }
    
    
    /*
    * The 64-bit jiffies value is not atomic - you MUST NOT read it
    * without sampling the sequence number in xtime_lock.
    * jiffies is defined in the linker script...
    */
    void do_timer(unsigned long ticks)
    {
    	jiffies_64 += ticks;
    	update_wall_time();
    	calc_global_load(ticks);
    }
    
    /**
    * update_wall_time - Uses the current clocksource to increment the wall time
    *
    * Called from the timer interrupt, must hold a write on xtime_lock.
    */
    static void update_wall_time(void)
    {
    	struct clocksource *clock;
    	cycle_t offset;
    	int shift = 0, maxshift;
    
    	/* Make sure we're fully resumed: */
    	if (unlikely(timekeeping_suspended))
    		return;
    
    	clock = timekeeper.clock;
    
    #ifdef CONFIG_ARCH_USES_GETTIMEOFFSET
    	offset = timekeeper.cycle_interval;
    #else
    	offset = (clock->read(clock) - clock->cycle_last) & clock->mask;
    #endif
    	timekeeper.xtime_nsec = (s64)xtime.tv_nsec << timekeeper.shift;
    
    	/*
    	 * With NO_HZ we may have to accumulate many cycle_intervals
    	 * (think "ticks") worth of time at once. To do this efficiently,
    	 * we calculate the largest doubling multiple of cycle_intervals
    	 * that is smaller then the offset. We then accumulate that
    	 * chunk in one go, and then try to consume the next smaller
    	 * doubled multiple.
    	 */
    	shift = ilog2(offset) - ilog2(timekeeper.cycle_interval);
    	shift = max(0, shift);
    	/* Bound shift to one less then what overflows tick_length */
    	maxshift = (8*sizeof(tick_length) - (ilog2(tick_length)+1)) - 1;
    	shift = min(shift, maxshift);
    	while (offset >= timekeeper.cycle_interval) {
    		offset = logarithmic_accumulation(offset, shift);
    		if(offset < timekeeper.cycle_interval<<shift)
    			shift--;
    	}
    
    	/* correct the clock when NTP error is too big */
    	timekeeping_adjust(offset);
    
    	/*
    	 * Since in the loop above, we accumulate any amount of time
    	 * in xtime_nsec over a second into xtime.tv_sec, its possible for
    	 * xtime_nsec to be fairly small after the loop. Further, if we're
    	 * slightly speeding the clocksource up in timekeeping_adjust(),
    	 * its possible the required corrective factor to xtime_nsec could
    	 * cause it to underflow.
    	 *
    	 * Now, we cannot simply roll the accumulated second back, since
    	 * the NTP subsystem has been notified via second_overflow. So
    	 * instead we push xtime_nsec forward by the amount we underflowed,
    	 * and add that amount into the error.
    	 *
    	 * We'll correct this error next time through this function, when
    	 * xtime_nsec is not as small.
    	 */
    	if (unlikely((s64)timekeeper.xtime_nsec < 0)) {
    		s64 neg = -(s64)timekeeper.xtime_nsec;
    		timekeeper.xtime_nsec = 0;
    		timekeeper.ntp_error += neg << timekeeper.ntp_error_shift;
    	}
    
    
    	/*
    	 * Store full nanoseconds into xtime after rounding it up and
    	 * add the remainder to the error difference.
    	 */
    	xtime.tv_nsec =	((s64) timekeeper.xtime_nsec >> timekeeper.shift) + 1;
    	timekeeper.xtime_nsec -= (s64) xtime.tv_nsec << timekeeper.shift;
    	timekeeper.ntp_error +=	timekeeper.xtime_nsec <<
    				timekeeper.ntp_error_shift;
    
    	/*
    	 * Finally, make sure that after the rounding
    	 * xtime.tv_nsec isn't larger then NSEC_PER_SEC
    	 */
    	if (unlikely(xtime.tv_nsec >= NSEC_PER_SEC)) {
    		xtime.tv_nsec -= NSEC_PER_SEC;
    		xtime.tv_sec++;
    		second_overflow();
    	}
    
    	/* check to see if there is a new clocksource to use */
    	update_vsyscall(&xtime, &wall_to_monotonic, timekeeper.clock,
    				timekeeper.mult);
    }
    
    
    /*
    * Called from the timer interrupt handler to charge one tick to the current
    * process.  user_tick is 1 if the tick is user time, 0 for system.
    */
    void update_process_times(int user_tick)
    {
    	struct task_struct *p = current;
    	int cpu = smp_processor_id();
    
    	/* Note: this timer irq context must be accounted for as well. */
    	account_process_tick(p, user_tick);
    	run_local_timers();
    	rcu_check_callbacks(cpu, user_tick);
    	printk_tick();
    #ifdef CONFIG_IRQ_WORK
    	if (in_irq())
    		irq_work_run();
    #endif
    	scheduler_tick();
    	run_posix_cpu_timers(p);
    }
    
    
    • 最重要的部分就是do_timer,以及其update_wall_time用於更新日期,而do_timer返回之前,update_process_times函數已經被調用,用於更新進程在過去的時間裏使用的各種統計值。

    • update_process_times通過傳入參數區分是內核狀態下的時間還是用戶狀態下的時間。

    • 0 表示系統
    • 1表示用戶
    • 這個值由tick_periodicupdate_process_times(user_mode(get_irq_regs()));代碼決定的。
    • 根據狀態
    /*
     * Account a single tick of cpu time.
     * @p: the process that the cpu time gets accounted to
     * @user_tick: indicates if the tick is a user or a system tick
     */
    void account_process_tick(struct task_struct *p, int user_tick)
    {
    	cputime_t one_jiffy_scaled = cputime_to_scaled(cputime_one_jiffy);
    	struct rq *rq = this_rq();
    
    	if (sched_clock_irqtime) {
    		irqtime_account_process_tick(p, user_tick, rq);
    		return;
    	}
    
    	if (user_tick)
    		account_user_time(p, cputime_one_jiffy, one_jiffy_scaled);
    	else if ((p != rq->idle) || (irq_count() != HARDIRQ_OFFSET))
    		account_system_time(p, HARDIRQ_OFFSET, cputime_one_jiffy,
    				    one_jiffy_scaled);
    	else
    		account_idle_time(cputime_one_jiffy);
    }
    
    • 統計對應執行狀態下的執行時間,這個不會更新jiffies.

    • 狀態有三種

      • 用戶態
      • 內核態
      • 空轉狀態
    • 可以使用指令查看

      time ps
      
      • 這個指令查看指令在各個過程中的消耗。
    • 上面的代碼說明了時間上並不嚴謹

      • 整個時間片都算在一個身上,有可能是內核與用戶之間切換了多次。
      • 甚至可能還不是一個進程執行這個時間片。
      • 現在這種程度已經差不多是Unix可以提供的最好的統計方式了。
      • 採用高頻率的切換可以提高統計精準度。
    • 執行過期定時器

      /*
       * Called by the local, per-CPU timer interrupt on SMP.
       */
      void run_local_timers(void)
      {
      	hrtimer_run_queues();
      	raise_softirq(TIMER_SOFTIRQ);
      }
      /*
       * Called from hardirq context every jiffy
       */
      void hrtimer_run_queues(void)
      {
      	struct timerqueue_node *node;
      	struct hrtimer_cpu_base *cpu_base = &__get_cpu_var(hrtimer_bases);
      	struct hrtimer_clock_base *base;
      	int index, gettime = 1;
      
      	if (hrtimer_hres_active())
      		return;
      
      	for (index = 0; index < HRTIMER_MAX_CLOCK_BASES; index++) {
      		base = &cpu_base->clock_base[index];
      		if (!timerqueue_getnext(&base->active))
      			continue;
      
      		if (gettime) {
      			hrtimer_get_softirq_time(cpu_base);
      			gettime = 0;
      		}
      
      		raw_spin_lock(&cpu_base->lock);
      
      		while ((node = timerqueue_getnext(&base->active))) {
      			struct hrtimer *timer;
      
      			timer = container_of(node, struct hrtimer, node);
      			if (base->softirq_time.tv64 <=
      					hrtimer_get_expires_tv64(timer))
      				break;
      
      			__run_hrtimer(timer, &base->softirq_time);
      		}
      		raw_spin_unlock(&cpu_base->lock);
      	}
      }
      
      
      • 先遍歷隊列中的定時器。
      • 然後置位軟中斷,處理時鐘中斷下半部。
    • 當前執行進程的時間片減少,合適的時候設置爲need_resched

      /*
       * This function gets called by the timer code, with HZ frequency.
       * We call it with interrupts disabled.
       *
       * It also gets called by the fork code, when changing the parent's
       * timeslices.
       */
      void scheduler_tick(void)
      {
      	int cpu = smp_processor_id();
      	struct rq *rq = cpu_rq(cpu);
      	struct task_struct *curr = rq->curr;
      
      	sched_clock_tick();
      
      	raw_spin_lock(&rq->lock);
      	update_rq_clock(rq);
      	update_cpu_load_active(rq);
      	curr->sched_class->task_tick(rq, curr, 0);
      	raw_spin_unlock(&rq->lock);
      
      	perf_event_task_tick();
      
      #ifdef CONFIG_SMP
      	rq->idle_at_tick = idle_cpu(cpu);
      	trigger_load_balance(rq, cpu);
      #endif
      }
      
      • 下面的宏定義表示是多核,如果是多核還會用於處理器負載平衡。
    • tick_periodic處理和架構相關的中斷處理函數,一般會進性清理,釋放鎖。

  • 日期

    • 相關模塊

    /*
    	kernel/time/timekeeping.c
    	struct timespec xtime
    */
    typedef long int __time_t;
    typedef long		__kernel_time_t;
    typedef __kernel_time_t		time_t;
    struct timespec {
    	__kernel_time_t	tv_sec;			/* seconds */
    	long		tv_nsec;		/* nanoseconds */
    };
    
    • 主要還是tv_sec,這個表示時間流逝了多久,時間的其實日期爲1970年一月一日。
    • tv_nsec,表示納秒。
    • 讀寫安全

    • 需要對應的xtime_lock,這個不是一個互斥鎖,而是一個順序鎖。針對的就是寫少讀多的情況。
    /*
     * Test if reader processed invalid data.
     *
     * If sequence value changed then writer changed data while in section.
     */
    static __always_inline int read_seqretry(const seqlock_t *sl, unsigned start)
    {
    	smp_rmb();
    
    	return unlikely(sl->sequence != start);
    }
    
    u64 get_jiffies_64(void)
    {
    	unsigned long seq;
    	u64 ret;
    
    	do {
    		seq = read_seqbegin(&xtime_lock);
    		ret = jiffies_64;
    	} while (read_seqretry(&xtime_lock, seq));
    	return ret;
    }
    
    /**
     * getnstimeofday - Returns the time of day in a timespec
     * @ts:		pointer to the timespec to be set
     *
     * Returns the time of day in a timespec.
     */
    void getnstimeofday(struct timespec *ts)
    {
    	unsigned long seq;
    	s64 nsecs;
    
    	WARN_ON(timekeeping_suspended);
    
    	do {
    		seq = read_seqbegin(&xtime_lock);
    
    		*ts = xtime;
    		nsecs = timekeeping_get_ns();
    
    		/* If arch requires, add in gettimeoffset() */
    		nsecs += arch_gettimeoffset();
    
    	} while (read_seqretry(&xtime_lock, seq));
    
    	timespec_add_ns(ts, nsecs);
    }
    
    • 獲取時間
    SYSCALL_DEFINE2(gettimeofday, struct timeval __user *, tv,
    		struct timezone __user *, tz)
    {
    	if (likely(tv != NULL)) {
    		struct timeval ktv;
    		do_gettimeofday(&ktv);
    		if (copy_to_user(tv, &ktv, sizeof(ktv)))
    			return -EFAULT;
    	}
    	if (unlikely(tz != NULL)) {
    		if (copy_to_user(tz, &sys_tz, sizeof(sys_tz)))
    			return -EFAULT;
    	}
    	return 0;
    }
    
    • 兩個入參是用於接收返回值的。
    • 內核提供的gettimeofday,而C庫提供了ftime,ctime這些。
    • 內核與日期
    • 內核除了更新xtime以外,幾乎沒有交集,因爲沒有必要。
    • 使用日期的是用戶應用,指的注意的是文件系統節點,存放的時間戳。
  • 定時器

    • 用於代碼的時間管理控制,特別是延時任務。

    • 中斷下半部的延時任務和這個任務有點不一樣,前者延時不確定,這個延時更精確。

    • 中斷下半部的主要目的是任務現在不執行,抽空執行。

    • 生命週期

      • 設置超時時間
      • 超時後的回調函數。
      • 超時後時鐘對象將會被銷燬。
      • 時鐘動態創建銷燬,時鐘數量沒有上限,內核中時鐘廣泛使用。
    • 使用

      • 結構體

        struct timer_list {
        	/*
        	 * All fields that change during normal runtime grouped to the
        	 * same cacheline
        	 */
        	struct list_head entry;
        	unsigned long expires;
        	struct tvec_base *base;
        
        	void (*function)(unsigned long);
        	unsigned long data;
        
        	int slack;
        
        #ifdef CONFIG_TIMER_STATS
        	int start_pid;
        	void *start_site;
        	char start_comm[16];
        #endif
        #ifdef CONFIG_LOCKDEP
        	struct lockdep_map lockdep_map;
        #endif
        };
        
        • 聲明在linux/timer.h
        • 定義在kernel/timer.c
      • 聲明變量

      • 用函數初始化

      • 設置超時,回調函數和參數。

        /* 聲明時鐘 */
        struct timer_list my_timer;
        /* 初始化時鐘 */
        init_timer(&my_timer);
        /* 賦值時鐘 */
        my_timer.expires = jiffies + delay; /* timer expires in delay ticks */
        my_timer.data = 0; /* zero is passed to the timer handler */
        my_timer.function = my_function; /* function to run when timer expires */
        /* 添加並激活時鐘 */
        add_timer(&my_timer);
        
        • 函數超時後會回調,回調在中斷下半部執行。
      • 當實時的jieffies大於等於定義的expires的時候,就會執行回調函數。函數執行沒有優先級一說,而且是異步執行,可能有的時鐘執行會有一點延遲。

      • 時鐘一般在超時之後就會執行,但是也有一些可能會有一定的延遲。這種不可能用於實時的程序。

      • 修改激活的時鐘

        • 通過函數mod_timer(&my_timer, jiffies + new_delay); 修改爲新的值。
        • 可以修改初始化沒有激活的,也可以修改激活的。如果沒有激活,這個函數還會激活這個時鐘。
      • 刪除激活的時鐘

        • del_timer(&my_timer);

          • 刪除的是非激活的返回0.
          • 否則返回1.
          • 如果刪除的是正在執行的,就存在競爭。
          • 刪除這樣可以等待執行完後刪除del_timer_sync。不過不能在中斷上下文。
        • 修改定時器不安全。

        • 多處理器刪除除非是知道沒有在執行。

        • 處理共享數據需要自旋鎖保護。

  • 定時器實現

    • 執行環境

      • 中斷上下文,時鐘中斷。
    • 執行步驟

      • 先處理完時鐘相關的上半部分。
      • 再處理完時鐘下半部,再處理定時器,所以定時器的執行並不是真的實時。存在一定的延遲。
      • 時鐘中斷處理進程的統計,等等信息。
      • run_timer_softirq將會執行所有的過期的定時器。
    • 定時器的存儲

      • 定時器是鏈式的,增刪查改的效率都不高。插入還要保證有序。
      • 所以內核將定時器根據過期時間分爲五個組。
      • 分拆保證了大多數情況下,查找的消耗不會太高。這樣就提高效率。
  • 延時實現

    • 需求

    • 需要精確的時間,而不是使用時鐘中斷,在中斷下半部執行不精確的延遲時間。
    • 採取的是根據硬件的頻率,來計算時間。
    • 比如一條指令執行需要1ns,那麼1000就是1us。精度遠遠高於1ms
    • 高精度延遲實現

    • 通過空轉,並禁用處理器的中斷,獲取精確延遲。這種精確延遲一般都不會很長時間的執行。
    • 另一種就不獲取處理器,但是精度並不保證。
    • 死循環

    • 使用while條件執行

      while(jiffies<delay);
      
      • jiffies是一個volatile類型,每次執行都會去所在地址讀取數據。
    • 切換處理器

      while(jiffies<delay)
      	cond_resched();
      
      • 當有更重要的任務的時候就第一時刻讓出處理器。
      • 等待下一次的執行。這個的話精度也不高。但是資源利用率比較高。
      • 有了進程切換所以就不能在中斷上下文環境中使用。也不能有鎖。
    • 精準計時

    • 精準計時會用處理器頻率計時,並且一直佔用,不被中斷。
    • 一般是短期計時,如果很長的高精度計時,則會進行切換。
    • 計時時睡眠schedule_timeout

    • 可以讓出處理器,但是這種精度不高。
    • 到時間了之後內核將喚醒放入執行隊列中。
    /* set task’s state to interruptible sleep */
    set_current_state(TASK_INTERRUPTIBLE);
    /* take a nap and wake up in “s” seconds */
    schedule_timeout(s * HZ);
    
    • 唯一參數是相對時間。
    • 設置的是可打斷的睡眠模式,遇到信號可以喚醒。
    • 設置了狀態還是在運行,切換出去了之後纔不會運行。
    • 因爲要切換進程所以不能有鎖(可能造成死鎖),只能在進程上下文中調用。
  • 發表評論
    所有評論
    還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
    相關文章