close
九、計時器
介紹驅動程式特有的時間管理機制。



9-1、Kernel 的計時器管理
kernel 管理的 timer 大致分成兩類:
  • 現在的日期與時間
  • jiffies
  • 其中 jiffies 指的就是 kernel 的 timer。




9-2、現在的日期與時間
「現在的日期與時間」指的是 1970 年 1月1日0時0分0秒至今為止的秒數,進而換算成日期與時間。

9-2.1、do_gettimeofday()
在 kernel 內想取得目前時刻,可以呼叫 do_gettimeofday():
void do_gettimeofday(struct timeval *tv);
timeval 結構定義在「linux/time.h」標頭檔內:
struct timeval {
time_t tv_sec; /* seconds */
suseconds_t tv_usec; /*microseconds */
};


do_gettimeofday 的範例:
#include
#include
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

static int sample_init(void) {
struct timeval now;
suseconds_t diff;
volatile int i;
printk(KERN_ALERT "devone driver installed: %s\n", __func__);

do_gettimeofday(&now);
diff = now.tv_usec; /* microseconds */

printk("Current UTC: %lu (%lu)\n", now.tv_sec, now.tv_usec);
for (i = 0; i < 999; i++)
;

do_gettimeofday(&now);
diff = now.tv_usec - diff;
printk("Elapsed time: %lu\n", diff);

return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "devone driver removed.\n");
}

module_init(sample_init);
module_exit(sample_exit);


9-2.2、2038 年問題
如果以 32-bit 變數存放經過的秒數,則會在 2038年1月19日 產生溢位,有可能導致程式運作出錯,稱為「2038年問題(Year 2038 problem; Y2K38)」。
解決的方式之一,是把 time_t 改成 64-bit,Linux 也是採用這個解法。



9-3、jiffies
OS 內部會使用 timer interrupt 定期執行某些 kernel 的工作。
timer interrupt 是使用硬體的計時功能,在指定的時間間隔向 OS 發出 interrupt。
linux 在收到 timer interrupt 時,會遞增 jiffies 變數。

9-3.1、HZ 巨集
前面提到 jiffies 是 timer interrupt 的次數紀錄。
timer interrupt 的間隔是由 HZ 巨集決定的,HZ 的意義就是「hertz」。

#ifdef __KERNEL__
define HZ CONFIG_HZ /* Internal kernel timer frequency */
define USER_HZ 100 /* .. some suer interfaces are in "ticks" */
define CLOCKS_PER_SEC /* like times() */
#endif


假設 HZ 的值是 100,這樣代表每秒 timer interrupt 會發生 100 次,也就是每 0.01 秒發生一次:
  • Timer interrupt 發生間隔 = 1/HZ (秒)

增加 HZ 的值時,就會增加 timer interrupt 的次數,也就是增加 kernel 內部需要處理的工作量。
kernel 內部需要定期處理各種工作,這些工作就是由 timer interrupt 定期引發的。
要縮短 kernel 的反應間隔,方法只有增加 timer interrupt 一途,但這樣會讓 kernel 消秏更多 CPU 資源。

9-3.2、遞增計數
實際增加 jiffies 的工作是由 do_timer() 完成的。
void do_timer(unsigned long ticks) {
jiffies_64 += ticks;
update_times(ticks);
}


9-3.3、驅動程式的取用方式
驅動程式想取用「jiffies」或「jiffies_64」,必須先 include 進「linux/jiffies.h」。
不過只要 include 進 linux/module.h 就會一併載入 jiffies.h ,所以不必明確 include 進 jiffies.h。

使用 jiffies 的範例:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed: %lu\n", jiffies);
return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "sample driver removed.\n");
}

module_init(sample_init);
module_exit(sample_exit);
}


9-3.4、497日問題
jiffies 會隨會時間不斷向上遞增,如果 HZ 巨集的時間設為 「100」 的話,每 10 毫秒就會有一次 timer interrupt,如果 jiffies 的值增加了「200」的話,就知道是經過「2秒」。
jiffies 定義為 unsigned long,在 IA-32 上是 32-bit 整數,而32-bit 整數的最大值是 4294967295(2^32-1),所以
  • 4294967295/100 => 42949672(秒)/86400 => 497(日)
這就稱為「497日問題」。

驅動程式如果用到 jiffies 的話,一定要特別注意這個問題,應對方式為:
  • 以 unsigned long 變數儲存 jiffies
  • 計算 timeout 時,只從 jiffies 減去先前的值


程式碼範例為:
#define OVER_TICKS (3*HZ)
unsigned long start_time = jiffies;
/* .... */
if (jiffies - start_time >= OVER_TICKS) {
... /* 經過三秒後要做的事 */
}


9-3.5、jiffies 初始值
linux 2.4 的 jiffies 是以 0 作為初始值,也知道 jiffies 會在第 497 天 overflow。
但為了徹底篩選出沒有處理 overflow 的程式碼, linux 2.6 開始把 jiffies 的初始值設為: 「-300秒」。
也就是說,只要五分鐘就會 overflow(變成 0 的時候就會 overflow)。



9-4、等待
有時為了讓硬體完成工作,驅動程式需要等待一段固定時間,此時可以使用定義在 linux/delay.h 的函式:
void mdelay(unsigned long msecs);
void udelay(unsigned long usecs);
void ndelay(unsigned long nsecs);

延遲的單位分別為毫秒(10^-3)、微秒(10^-6)、奈秒(10^-9)。

這些等待函式,在等待的過程中都會佔住 CPU,不會讓 CPU 去處理其它工作,這樣子的方式,稱為「busy waiting(忙錄等待)」。
系統如果只有一個 CPU 的話,等待時,會讓整個 OS 停住(但是可以處理 interrupt),所以一般的認知為:
  • 不要長時間 busy waiting
busy waiting 的好處是可以在 interrupt context 使用。

busy waiting 的範例:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed: %lu\n", jiffies);

mdelay(100);
udelay(1000);
ndelay(1000);

printk(KERN_ALERT "%lu (HZ %d)\n", jiffies, HZ);
return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "sample driver removed.\n");
}

module_init(sample_init);
module_exit(sample_exit);


9-4.1、不忙碌的等待
如果在等待過程中,希望 CPU 去做其它事的話,可以用定義在「linux/delay.h」中 sleep 系列的等待函式:
void ssleep(unsigned int seconds);
void msleep(unsigned int msecs);
unsigned long msleep_interruptible(unsigned int msecs);

延遲的單位分別為秒、毫秒(10^-3)、毫秒(10^-3)。
msleep_interruptible() 與 msleep() 不同的地方在於等待過程可能被中斷,中斷指的是 process 可以接收 signal。
如果在 msleep_interruptible() 中收到 signal 而中斷等待的話,則會回傳距離原始時限的時間(正值),否則的話傳回「0」。

這幾個函式都會使 driver context 進入 sleep,所以不能在 interrupt context 中使用,因為 interrupt context 位於最高級,如果在 interrupt 處理時式之內 sleep 的話,就不會執行其它 process,也不會有其它人來叫醒 interrupt 處理程式,會造成全系統鎖死。

sleep() 的範例:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed: %lu\n", jiffies);
#if ssleep
ssleep(10);
#else
ret = msleep_interruptible(10*1000); /* sleep for 10 seconds */
#endif

printk(KERN_ALERT "%lu (HZ %d)\n", jiffies, HZ);
return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "sample driver removed.\n");
}

module_init(sample_init);
module_exit(sample_exit);


在執行 ssleep() 時,終端就就算按「Ctrl+C」鍵送出訊號也不會造成中斷。
以 ps 來看 insmod 的狀態,會發現是 「D(uninterruptible sleep)」。
而在執行 msleep_interruptible() 時,若按「Ctrl+C」鍵送出中斷訊號,則會發現 process 被中斷了,並回傳正值。
以 ps 來看 insmod 的狀態,會發現是 「S(interruptible sleep)」。



9-5、Kernel Timer
驅動程式在對硬體送出 DMA 指令後,為了監視 DMA 執行完成,有時需要在一定時間過後呼叫某些函式,linux kernel 有為這類計時需求提供機制,這個機制就是「kernel timer」。
舉例來說,驅動程式對 I/O 裝置下令開始 DMA 後,通常在 I/O 裝置完工後,都會送出 interrupt 讓驅動程式繼續作處理。
不過驅動程式有個鐵則,那就是:
  • 不能假定硬體一定能正確完成工作
I/O 裝置有可能故障或發生其它問題,這時也不能讓 OS 當掉,必須重新,讓應用程式可以繼續執行,或是通知應用程式硬體發生問題。
應用程式對驅動程式發出請求的情況下,驅動程式必須對這個請求送出回應,如果沒有在時限內收到 interrupt,必須能偵測出這種情形,並將錯誤碼回報給應用程式。

9-5.1、Kernel Timer 的使用方式
kernel timer 的使用方式為:
  • 準備 timer_list 結構變數
  • 撰寫 timeout 處理函式
  • 設定時限並啟動 timer
其中 timer_list 結構變數不能是區域變數,必須是全域變數或配置在 heap 之內,timer_list 結構定義於「linux/timer.h」,主要成員包含:
  • expires
  • function
  • data

timer_list 的初始化工作必須使用 kernel 提供的 init_timer() 函式:
void init_timer(struct timer_list *timer);
Timeout 處理函式的 prototype 如下:
void (*function) (unsigned long);
它會收到一個 unsigned long 型別的引數,如果要傳遞許多引數時,使用結構指標即可。

「expires」成員是時限(單位是 jiffies),也就是呼叫 timeout 處理函式的時間,舉例來說,如果要在五秒後呼叫 timeout 函式的話:
expires = jiffies + 5*HZ
雖然沒有考慮到 overflow 的情形,不過 kernel 會做適當的處理。
呼叫 add_timer() 就會開始計時:
void add_timer(struct timer_list *timer);

Timer 範例程式:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

#define TIMEOUT_VALUE (10*HZ)

static struct timer_list tickfn;

static void sample_timeout(unsigned long arg) {
struct timer_list *ptr = (struct timer_list *)arg;
printk("ptr %p (%s) \n", ptr, __func__);
}

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed.\n");

init_timer(&tickfn);
tickfn.function = sample_timeout;
tickfn.data = (unsigned long)&tickfn;
tickfn.expires = jiffies + TIMEOUT_VALUE;
add_timer(&tickfn);

return 0;
}

static void sample_exit(void) {
printk(KERN_ALERT "sample driver removed.\n");
}

module_init(sample_init);
module_exit(sample_exit);


試著編譯、載入後,注意 syslog,可以看到剛好過了 10 秒後呼叫 timeout 函式。
通常在經過指定時間後,不會剛好在「那一刻」呼叫 timeout 函式,因為此時, kernel 可能在忙著處理其它事情,就沒辦法立刻轉來呼叫 timeout 函式,因為 kernel 並不是 preemptive。

9-5.2、卸除驅動程式時必須注意
上述範例,有個致命的問題,如果在 kernel 呼叫 timeout 函式之前,卸除驅動程式的話,會發生什麼事呢?
kernel panic。

因此在卸除驅動程式時,如果有以下情形的話:
  • 還有 timeout 函式沒被呼叫
  • 正在執行 timeout 函式
就必須先拿掉 timeout 函式、或將之停止才行。

為此,kernel 提供了 del_timer_sync() 函式:
int del_timer_sync(struct timer_list *timer);
回傳值有兩種形式:
傳回值意義
0成功卸除函式,而 timeout 函式已被呼叫並執行完畢
正值在呼叫 timeout 函式之前已將之卸除

使用這個函式時,須注意:
  • 不能在 interrupt context 之內呼叫
  • 不能在拿著 spin lock 時呼叫

因此,結論是,在驅動程式使用 kernel timer 時,一定要記得讓 add_timer() 與 del_timer_sync() 成對出現。

修正過的 Timer 範例程式:
#include
#include

MODULE_LICENSE("Dual BSD/GPL");

#define TIMEOUT_VALUE (10*HZ)

static struct timer_list tickfn;

static void sample_timeout(unsigned long arg) {
struct timer_list *ptr = (struct timer_list *)arg;
printk("ptr %p (%s) \n", ptr, __func__);
}

static int sample_init(void) {
printk(KERN_ALERT "sample driver installed.\n");

init_timer(&tickfn);
tickfn.function = sample_timeout;
tickfn.data = (unsigned long)&tickfn;
tickfn.expires = jiffies + TIMEOUT_VALUE;
add_timer(&tickfn);

return 0;
}

static void sample_exit(void) {
int ret;
ret = del_timer_sync(&tickfn);
printk(KERN_ALERT "sample driver removed (%d)\n", ret);
}

module_init(sample_init);
module_exit(sample_exit);


9-5.3、Kernel Timer 的 Context
timeout 函式會在 interrupt context 執行,所以 timeout 函式需注意:
  • 不能呼叫會 sleep 的函式
  • 不能使用 user context 的 spin lock
  • 如果有多個 CPU,則可能同時執行多個 timeout 函式

9-5.4、Interval Timer
kernel timer 是 one-shot timer,也就是發生一次 timeout 之後就不會再呼叫第二次 timeout 函式,如果要達到每隔一段時間連續呼叫的話,可以:
  • 在 timeout 函式的最後,呼叫 mod_timer() 重設 timer:

    • int mod_timer(struct timer_list *timer, unsigned long expires); 函式回傳值有兩種形式,不過都不代表失敗情形:
      傳回值意義
      0 timer 是「inactive」,要重設的 timer 還沒加進 kernel
      1 timer 是「active」,要重設的 timer 已經加進 kernel

      事實上, mod_timer() 的處理內容其實與下面這樣程式碼相等:
      del_timer(timer);
      timer->expires = expires;
      add_timer(timer);


      Interval timer 範例程式:
      #include
      #include

      MODULE_LICENSE("Dual BSD/GPL");

      #define TIMEOUT_VALUE (10*HZ)

      static struct timer_list tickfn;

      static void sample_timeout(unsigned long arg) {
      struct timer_list *ptr = (struct timer_list *)arg;
      int ret;
      ret = mod_timer(tick, jiffies + TIMEOUT_VALUE);
      printk("ptr %p (%s) \n", ptr, __func__);
      }

      static int sample_init(void) {
      printk(KERN_ALERT "sample driver installed.\n");

      init_timer(&tickfn);
      tickfn.function = sample_timeout;
      tickfn.data = (unsigned long)&tickfn;
      tickfn.expires = jiffies + TIMEOUT_VALUE;
      add_timer(&tickfn);

      return 0;
      }

      static void sample_exit(void) {
      int ret;
      ret = del_timer_sync(&tickfn);
      printk(KERN_ALERT "sample driver removed (%d)\n", ret);
      }

      module_init(sample_init);
      module_exit(sample_exit);


      可以利用 cmd: cat /proc/kmsgcat /var/log/messages 來查看 printk 的 log 訊息。

      9-5.5、注意「錯過」情形
      使用 kernel timer 時,必須特別注意「錯過」的情形,也就是「race condition」或簡稱「racing」。

      舉例來說,在 user process 送出 read 請求,準備讀取資料的情形後,驅動程式收到 read 請求,會接著向硬體(I/O裝置) 送出 DMA 指令,並進入 sleep 狀態,等到 DMA 完成,硬體會發出 interrupt 通知驅動程式,接著驅動程式開始讀取資料,並把資料送給 user process。
      在驅動程式進入 sleep 狀態的同時,會有一個 timer ,如果超過一定的時間,還沒被硬體發出的 interrupt 喚醒,則會在 timeout 的情況下被喚醒。
      這個例子硬體所發出的 interrupt 來喚醒驅動程式,和驅動程式因為 timeout 情況下被喚醒,就是造成 racing 的兩個原因。
      由於並不知道 interrupt 何時會發生,所以設計時,必須考慮不同的時機,避免競爭狀態才行。

      舉一個 character 類型的驅動程式,讀取裝置檔時會不斷傳回「0xFF」,透過 timer interrupt 來模擬。
      載入驅動程式的步驟如下:
      • 以 insmod 載入:# insmod ./devone.ko
      • 在 syslog 查閱 major number 並紀錄
      • 建立裝置檔:# mknod /dev/devone c 0
      • 讀取裝置檔:# hexdump -C -n 16 /dev/devone
      • 卸除驅動程式:# rmmod devone

      驅動程式可以接收引數,修改 interrupt 的時機與等待時限,比如說,這樣執行的話,會將 timeout 的值設為3 秒、interrupt 發生的時間設為 10 秒:
      # insmod ./devone.ko timeout_value=3 irq_value=10

      以預設的情形執行,就是將 timeout 設為 10 秒,IRQ 在 3 秒時發生;如果要模擬讀取資料超過時限的情形,就是更改 timer 與 IRQ 設定的方式來執行:
      #sudo /sbin/insmod ./devone.ko
      #sudo /sbin/insmod ./devone.ko timeout_value=3 irq_value=10


      以下列指令來確認讀取資料時情形:
      sudo tail /var/log/messages
      sudo mknod /dev/devone c 250 0
      hexdump -C -n 16 /dev/devone
      sudo tail /var/log/messages
      sudo /sbin/rmmod devone
      sudo tail /var/log/messages


      考慮到 racing 的 timer 範例程式:
      #include
      #include
      #include
      #include
      #include
      #include
      #include
      #include
      #include

      MODULE_LICENSE("Dual BSD/GPL");

      static unsigned int timeout_value = 10;
      static unsigned int irq_value = 3;

      module_param(timeout_value, uint, 0);
      module_param(irq_value, uint, 0);

      static int devone_devs = 1;
      static int devone_major = 0; /* 0=dynamic allocation */
      static struct cdev devone_cdev;

      struct devone_data {
      struct timer_list timeout;
      struct timer_list irq;
      spinlock_t lock;
      wait_queue_head_t wait;
      int dma_done;
      int timeout_done;
      };

      static void devone_timeout(unsigned long arg) {
      struct devone_data *dev = (struct devone_data *)arg;
      unsigned long flags;

      spin_lock_irqsave(&dev->lock, flags);
      printk("%s called\n", __func__);
      dev->dma_done = 1;
      wake_up(&dev->wait);
      spin_unlock_irqrestore(&dev->lock, flags);
      }

      static void devone_irq(unsigned long arg) {
      struct devone_data *dev = (struct devone_data *)arg;
      unsigned long flags;

      spin_lock_irqsave(&dev->lock, flags);
      printk("%s called\n", __func__);
      dev->dma_done = 1;
      wake_up(&dev->wait);
      spin_unlock_irqrestore(&dev->lock, flags);
      }

      static void devone_dma_transfer(struct devone_data *dev) {
      dev->dma_done = 0;
      mod_timer(&dev->irq, jiffies + irq_value*HZ);
      }

      ssize_t devone_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos) {
      printk(KERN_ALERT "%s called\n", __func__);
      struct devone_data *dev = filp->private_data;
      int i;
      unsigned char val = 0xff;
      int retval;

      /* start timer */
      dev->timeout_done = 0;
      mod_timer(&dev->timeout, jiffies + timeout_value*HZ);

      /* kick DMA */
      devone_dma_transfer(dev);

      /* sleep process with condition */
      wait_event(dev->wait, (dev->dma_done == 1) || (dev->timeout_done == 1));

      /* cancel timer */
      del_timer_sync(&dev->timeout);

      if (dev->timeout_done == 1)
      return -EIO;

      /* store read data */
      for (i = 0; i < count; i++) {
      if (copy_to_user(&buf[i], &val, 1)) {
      retval = -EFAULT;
      goto out;
      }
      }
      retval = count;
      out:
      return (retval);
      }

      int devone_open(struct inode *inode, struct file *filp) {
      struct devone_data *dev;

      dev = kmalloc(sizeof(struct devone_data), GFP_KERNEL);
      if (dev == NULL)
      return -ENOMEM;

      /* initialize members */
      spin_lock_init(&dev->lock);
      init_waitqueue_head(&dev->wait);
      dev->dma_done = 0;
      dev->timeout_done = 0;

      init_timer(&dev->timeout);
      dev->timeout.function = devone_timeout;
      dev->timeout.data = (unsigned long)dev;

      init_timer(&dev->irq);
      dev->irq.function = devone_irq;
      dev->irq.data = (unsigned long)dev;

      filp->private_data = dev;

      return 0;
      }

      int devone_close(struct inode *inode, struct file *filp) {
      struct devone_data *dev = filp->private_data;

      if (dev) {
      del_timer_sync(&dev->timeout);
      del_timer_sync(&dev->irq);
      kfree(dev);
      }
      return 0;
      }

      struct file_operations devone_fops = {
      .open = devone_open,
      .release = devone_close,
      .read = devone_read,
      };

      static int sample_init(void) {
      dev_t dev = MKDEV(devone_major, 0);
      int ret;
      int major;
      int err;

      ret = alloc_chrdev_region(&dev, 0, devone_devs, "devone");
      if (ret < 0)
      return ret;
      devone_major = major = MAJOR(dev);

      cdev_init(&devone_cdev, &devone_fops);
      devone_cdev.owner = THIS_MODULE;
      devone_cdev.ops = &devone_fops;
      err = cdev_add(&devone_cdev, MKDEV(devone_major, 0), 1);
      if (err)
      return err;

      printk(KERN_ALERT "devone driver(major %d) installed.\n", major);
      printk(KERN_ALERT "timeout %u irq %u timer (%s)\n", timeout_value, irq_value, __func__);

      return 0;
      }

      static void sample_exit(void) {
      dev_t dev = MKDEV(devone_major, 0);
      printk(KERN_ALERT "devone driver removed.\n");
      cdev_del(&devone_cdev);
      unregister_chrdev_region(dev, devone_devs);
      }

      module_init(sample_init);
      module_exit(sample_exit);

      目前執行起來, read() 是無法 work 的,不確定原因…,過陣子再回過頭來解。

      上述範例準備了兩個 timer 函式,devone_interrupt() 是偵測時限使用,devone_irq() 則是模擬 interrupt 使用,在 process 打開裝置檔(/dev/devone)時,會初始化這兩個 timer 函式,但當 process 實際 read 時,才會真正啟動 timer。
      透過 wait_event() 讓 process 進入 sleep 狀態。
      process 被喚醒後,首先會取消 timer 函式,接著檢查收到的是 interrupt 還是 timeout,來決定回傳的狀態碼。
      最後是執行 close() 處理函式中釋放資源的動作。



      9-6、結語
      應用程式如果需要每隔一段時間作什麼事的話,只要建立 thread 反覆等待執行即可,但驅動程式不能用這種作法。
      在 kernel space 內,如果不平行執行工作的話,就無法提供 OS 功能了。
      kernel timer 使用時雖然有些限制、麻煩,但卻能讓驅動程式更加靈活、提供更多功能。
arrow
arrow
    全站熱搜

    silverfoxkkk 發表在 痞客邦 留言(1) 人氣()