2009年2月15日 星期日

libusb 的底層追蹤 (libusb thread support and the relation with kernel usbfs)

因為做 project,無意間找到了Greg KH 大師級的文章-- [Snooping the USB Data Stream] 文中有一段提到 kernel 對 usbfs 的支援,讓 application 可直接透過 usbfs 對 device 發出 usb transfer,實做於devio.c, inode.c, and devices.c 等三個 kernel sources. (note: 我這裡的 kernel version 是 2.6.26)


而另一方面,在 application library 端,就是依靠 usbfs 的幫忙,發展出 [libusb project] 1.0.0  版,但在 debian testing 的套件裏,還只包到 libusb-0.1.12,若要 library header 檔,請安裝 libusb-dev 套件。

從 0.1.12 到 1.0.0 做了許多改變,除了 API 幾乎重新定之外,最明顯的是增加了 thread 的支援,但是兩者在最底層的系統呼叫,都是使用了 usbfs 提供的 I/O control。因為 1.0.0 的設計蠻複雜的,所以我還是以 0.1.12 來追蹤(其實是功力不夠啦!)以下就是一些 0.1.12 追蹤的筆記:

in libusb-0.1.12:

usb_bulk_read()         
usb_interrupt_read()
usb_bulk_write()   與    usb_interrupt_write()
       :                      :
       V                      V
   USB_URB_TYPE_BULK      USB_URB_TYPE_INTERRUPT
                 :           :
                 :           :
                 V           V
               usb_urb_transfer()

usb_urb_transfer() 大致上流程,僅僅提供了 synchronous 的傳送方式(就是呼叫之後就等待它完成)---在 0.1.12 的介面上並沒有提供 asynchronous 的方式(就是呼叫後就離開,將來 urb 收/送完成後,系統會呼叫 complete function),注意:這是 libusb-0.1.12 並沒有提供 asynchronous 的函數,但是 kernel 的 IOCTL_USB_SUBMITURB 工作方式卻都是 asynchronous 的動作,等下追蹤 kernel 的部份時就會知道了。
usb_urb_transfer() 用 IOCTL_USB_SUBMITURB 送出 urb 之後,然後一直重複使用 IOCTL_USB_REAPURBNDELAY 來收取completed urb  ,並且把使用者傳入的 timeout 切成 1ms 的單位用 select() 來等待。結果有幾種:
1. select() 等到了 I/O 動作,REAPURB 得到了某個 completed urb ,返回值是所收送的 data 長度。
2. select() 等不到 I/O 動作,重複 1ms 的 select()等待,一直到 timeout 了,返回值是 -ETIMEDOUT

usb_urb_transfer() 有一段註解,是這樣說的,直接節錄下來:

#define URB_USERCONTEXT_COOKIE        ((void *)0x1)

  /*
   * HACK: The use of urb.usercontext is a hack to get threaded applications
   * sort of working again. Threaded support is still not recommended, but
   * this should allow applications to work in the common cases. Basically,
   * if we get the completion for an URB we're not waiting for, then we update
   * the usercontext pointer to 1 for the other threads URB and it will see
   * the change after it wakes up from the the timeout. Ugly, but it works.
   */

雖然這裡說 Threaded application 可以 work ! 根據這段註解說,使用了 urb.usercontext 來"標示" reapped urb---若不是我們的 urb 就把該 urb.usercontext 設定為 URB_USERCONTEXT_COOKIE,以便讓另一個 thread 可以 reap:但是另一個 thread 可以 reap 到這個作了 cookie 記號的 urb 嗎?

追蹤到這裡,我們不得不往 kernel 的 devio.c 追蹤,要徹底了解 kernel 提供 usbfs 的動作才能解答這個問題...

首先從  IOCTL_USB_SUBMITURB 開始找線索,因為這個是 libusb 定義的 I/O control code,kernel 裏相對應的是 USBDEVFS_SUBMITURB I/O control code,負責處理這個 I/O control code  的是 proc_submiturb(ps, p); 其中 ps 與 p 分別是

   struct dev_state *ps = file->private_data;
   void __user *p = (void __user *)arg;

arg 是 user 由 ioctl system call 傳入的 argument pointer,這裡傳入 user urb。
ps 是 usbdev_open() 時 allocated 得到的,定義為 :

struct dev_state {
    struct list_head list;      /* state list */
    struct usb_device *dev;
    struct file *file;
    spinlock_t lock;            /* protects the async urb lists */
    struct list_head async_pending;
    struct list_head async_completed;
   
wait_queue_head_t wait;     /* wake up if a request completed */
    unsigned int discsignr;
    struct pid *disc_pid;
    uid_t disc_uid, disc_euid;
    void __user *disccontext;
    unsigned long ifclaimed;
    u32 secid;
};


可以把這個資料結構看成 process 對於這個 device 的傳送 urb 的狀態紀錄. 其中兩個 list 分別是 urb 送出後就把對應的 async 由 async_pending 紀錄,等 urb complete 之後就把對應的 async 由 async_completed 紀錄,async 是甚麼呢?對於每個 urb 都有一個對應的 async data structure,是在 proc_do_submiturb() 時 allocate 得到的 。async 的資料結構為:

struct async {
    struct list_head asynclist;
    struct dev_state *ps;
    struct pid *pid;
    uid_t uid, euid;
    unsigned int signr;
    unsigned int ifnum;
    void __user *userbuffer;
    void __user *userurb;
    struct urb *urb;

    int status;
    u32 secid;
};  

對每個 urb 都有一個 async 紀錄,在 proc_do_submiturb() 時 allocate 得到,同時它也會紀錄 user urb 的位置,將來可以把 urb 所得的資料 copy 回 user urb。


整個 urb 的流程為:
proc_submiturb()
: 把 user's urb(user space) 拷貝一份到我們的 uurb (kernel space)
->proc_do_submiturb(): 根據 bulk,interrupt..等 type 分別 initial 一些欄位,然後 allocate async data structure (裡面還包含 urb),然後放入 ps->async_pending queue 做紀錄,接著呼叫 usb_submit_urb() (之後就交給 usb host controller 處理了)然後不等結果就返回, 所以我們說 kernel 這裡是以 asynchronous 的方式處理 urb !

等 usb host controller 把 urb 處理完後,會呼叫 async_complete(),async_complete() 將 async 紀錄從 ps->async_pending 移到 ps->completed.

另一方面 user 要透過 IOCTL_USB_REAPURBNDELAY "收割" 已完成的 urb ,對應到 kernel 為 USBDEVFS_REAPURBNDELAY。這個 I/O control 會呼叫 proc_reapurbnonblock(),它會巡視 ps->completed 是否有 async 紀錄,若無則返回 -EAGAIN,若有則把找到的 urb (kernel space) 資料拷貝到 user space,並設定 IOCTL_URB_REAPURBNDELAY 時傳入的 arg 指向 user space urb,至此,kernel 傳送的部份已經完成。

好,kernel 的部份至此大致了解,接下來討論我們的疑問,我們分兩種情形討論:
1. 每一個 thread 都使用同一個 open handle, 也就是在 kernel 裡面同一份 ps:
  當 IOCTL_USB_REAPURBNDELAY 時,在 kernel 裡,會取出 ps->completd 上已完成 urb ,但是並不知道是哪一個 thread 的 urb ,因此 libusb 使用 cookie 作記號;
在 usb_urb_transfer() 時,每一個 thread 在自己的 thread stack 上宣告一個 user space urb,如果第一個 thread reap 到不屬於自己的 urb 就打上 cookie 並繼續從 kernel reap 其他的 urb,此時當第二個 thread reap urb 時,就從 ps->completed 再抓一個 ... of course ,這樣第二個 thread 就收不到該收的 urb 了,錯誤就產生了。

2.
每一個 thread 各自開了 open handle, 也就是在 kernel 裡面對應各自的 ps:
  這樣每個 thread 都對應了自己的 ps->completed list,所收的 urb 不會混淆,好像也不需要 cookie 了喔!但是這樣有一個缺點:bulk transfer 之前要 claim interface---就是每個 thread 要使用 usb_urb_transfer() 時必須先 claim interface...,所以原來已經 claim interface 的 thread 要 release interface,那就要把之前該 thread 的 urb 結束掉才行囉,這樣多個 thread "interleave urbs transfer" 的本意就沒了啊!

所以結論是:若想以 thread 的方式使用 usblib-0.1.12,大概自己要動手改 usb_urb_transfer() 的部份,否則就直接用 usblib-1.0.0 。



沒有留言:

張貼留言