深入select多路複用內核源碼加驅動實現

原文鏈接:https://my.oschina.net/fileoptions/blog/911091

 

問題

     本文嘗試解決以下幾個問題:

  • select中1024限制指的是select監聽的文件描述符個數最多爲1024還是說監聽的最大文件描述符不能超過1024?

  • select第一個參數爲什麼是最大文件描述符加一?

  • select的返回值表示的是就緒的文件描述符個數還是就緒的事件個數?

  • 一個設備或資源要支持select機制,驅動需要做哪些實現和改變?

概述

      用戶空間在調用select庫函數之後,最終會陷入內核態調用系統調用函數(sys_select),如果對系統調用原理感興趣的可以看我的另一篇文章從glibc源碼看系統調用原理,下面先大致看一下select中內核函數都有哪些。

                       

                      

                         

源碼實現

#undef __NFDBITS
#define __NFDBITS    (8 * sizeof(unsigned long))

#undef __FD_SETSIZE
#define __FD_SETSIZE    1024

#undef __FDSET_LONGS
#define __FDSET_LONGS    (__FD_SETSIZE/__NFDBITS)

 
typedef struct {
    unsigned longfds_bits [__FDSET_LONGS];   //1024個bit。可以看到可以支持1024個描述符
} __kernel_fd_set;

//系統調用(內核態)
//參數爲 maxfd, r_fds, w_fds, e_fds, timeout。
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, struct timeval __user *tvp)
{
    s64 timeout = -1;
    struct timeval tv;
    int ret;

    //將超時時間換成jiffies
    if (tvp) {
        if (copy_from_user(&tv, tvp, sizeof(tv))) //將用戶態參數拷貝到內核態
            return -EFAULT;
         if (tv.tv_sec < 0 || tv.tv_usec < 0)
            return -EINVAL;
         /* Cast to u64 to make GCC stop complaining */
        if ((u64)tv.tv_sec >= (u64)MAX_INT64_SECONDS)
            timeout = -1;    /* infinite */
        else {
            timeout = ROUND_UP(tv.tv_usec, USEC_PER_SEC/HZ);
            timeout += tv.tv_sec * HZ;
        }
    }
    // (***) 調用 core_sys_select
    ret = core_sys_select(n, inp, outp, exp, &timeout);

    //將剩餘時間拷貝回用戶空間進程
    if (tvp) {
        struct timeval rtv;
        if (current->personality & STICKY_TIMEOUTS) //判斷當前環境是否支持修改超時時間(不確定)
            goto sticky;
        rtv.tv_usec = jiffies_to_usecs(do_div((*(u64*)&timeout), HZ));
        rtv.tv_sec = timeout;
        if (timeval_compare(&rtv, &tv) >= 0)
            rtv = tv;
        if (copy_to_user(tvp, &rtv, sizeof(rtv))) {
sticky:
            /*
             * 如果應用程序將timeval值放在只讀存儲中,
             * 我們不希望在成功完成select後引發錯誤(修改timeval)
             * 但是,因爲沒修改timeval,所以我們不能重啓這個系統調用。
             */
            if (ret == -ERESTARTNOHAND)
                ret = -EINTR;
        }
    }
    return ret;
}
//主要的工作在這個函數中完成
staticint core_sys_select(int n, fd_set __user *inp, fd_set __user *outp, fd_set __user *exp, s64 *timeout)
{
    fd_set_bits fds;
    /*  fd_set_bits 結構如下:
     typedef struct {
         unsigned long *in, *out, *ex;
         unsigned long *res_in, *res_out, *res_ex;
    } fd_set_bits;

    這個結構體中定義的全是指針,這些指針都是用來指向描述符集合的。

     */

    void *bits;
    int ret, max_fds;
    unsigned int size;
    struct fdtable *fdt;
    /* Allocate small arguments on the stack to save memory and be faster 先嚐試使用棧(因爲棧省內存且快速)*/

    long stack_fds[SELECT_STACK_ALLOC/sizeof(long)];  // SELECT_STACK_ALLOC=256

    ret = -EINVAL;
    if (n < 0)
        goto out_nofds;

    /* max_fds can increase, so grab it once to avoid race */
    rcu_read_lock(); //rcu鎖

    fdt = files_fdtable(current->files); //讀取文件描述符表
    /*  struct fdtable 結構如下:
    struct fdtable {
       unsigned int max_fds;
       struct file **fd;
       ...
    };
     */

    max_fds = fdt->max_fds; //從files結構中獲取最大值(當前進程能夠處理的最大文件數目)
    rcu_read_unlock();
    if (n > max_fds)// 如果傳入的n大於當前進程最大的文件描述符,給予修正
        n = max_fds;

    /* 我們需要使用6倍於最大描述符的描述符個數,
     * 分別是in/out/exception(參見fd_set_bits結構體),
     * 並且每份有一個輸入和一個輸出(用於結果返回) */
    size = FDS_BYTES(n);// 以一個文件描述符佔一bit來計算,傳遞進來的這些fd_set需要用掉多少個字
    bits = stack_fds;
    if (size > sizeof(stack_fds) / 6) { // 除以6,因爲每個文件描述符需要6個bitmaps上的位。
        //棧不能滿足,先前的嘗試失敗,只能使用kmalloc方式
        /* Not enough space in on-stack array; must use kmalloc */
        ret = -ENOMEM;
        bits = kmalloc(6 * size, GFP_KERNEL);
        if (!bits)
            goto out_nofds;
    }
    //設置fds
    fds.in      = bits;
    fds.out     = bits +   size;
    fds.ex      = bits + 2*size;
    fds.res_in  = bits + 3*size;
    fds.res_out = bits + 4*size;
    fds.res_ex  = bits + 5*size;

    // get_fd_set僅僅調用copy_from_user從用戶空間拷貝了fd_se
    if ((ret = get_fd_set(n, inp, fds.in)) ||
        (ret = get_fd_set(n, outp, fds.out)) ||
        (ret = get_fd_set(n, exp, fds.ex)))
        goto out;

    // 對這些存放返回狀態的字段清0
    zero_fd_set(n, fds.res_in);
    zero_fd_set(n, fds.res_out);
    zero_fd_set(n, fds.res_ex);

    // 執行do_select,完成監控功能
    ret = do_select(n, &fds, timeout);
    if (ret < 0) // 有錯誤
        goto out;
    if (!ret) {  // 超時返回,無設備就緒
        ret = -ERESTARTNOHAND;
        if (signal_pending(current))
            goto out;
        ret = 0;
    }

    if (set_fd_set(n, inp, fds.res_in) ||
        set_fd_set(n, outp, fds.res_out) ||
        set_fd_set(n, exp, fds.res_ex))
        ret = -EFAULT;

out:
    if (bits != stack_fds)
        kfree(bits);

out_nofds:
    return ret;
}
#define POLLIN_SET (POLLRDNORM | POLLRDBAND | POLLIN | POLLHUP | POLLERR)
#define POLLOUT_SET (POLLWRBAND | POLLWRNORM | POLLOUT | POLLERR)
#define POLLEX_SET (POLLPRI)

int do_select(int n, fd_set_bits *fds, s64 *timeout)
{
    struct poll_wqueues table;

    /*
     struct poll_wqueues {
          poll_table pt;
          struct poll_table_page *table;
          struct task_struct *polling_task; //保存當前調用select的用戶進程struct task_struct結構體
          int triggered;         // 當前用戶進程被喚醒後置成1,以免該進程接着進睡眠
          int error;             // 錯誤碼
          int inline_index;      // 數組inline_entries的引用下標
          struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
    };

     */

    poll_table *wait;
    int retval, i;
    rcu_read_lock();
    //根據已經設置好的fd位圖檢查用戶打開的fd, 要求對應fd必須打開, 並且返回最大的fd。
    retval = max_select_fd(n, fds);
    rcu_read_unlock();
    if (retval < 0)
        return retval;

    n = retval;
    /* 一些重要的初始化:
       poll_wqueues.poll_table.qproc函數指針初始化,
       該函數是驅動程序中poll函數(fop->poll)實現中必須要調用的poll_wait()中使用的函數。  */
    poll_initwait(&table);
    wait = &table.pt;
    if (!*timeout)
        wait = NULL;        // 用戶設置了超時時間爲0
    retval = 0;

    for (;;) {
        unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
        long __timeout;
        set_current_state(TASK_INTERRUPTIBLE);
        inp = fds->in; outp = fds->out; exp = fds->ex;
        rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;
        // 所有n個fd的循環
        for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
            unsigned long in, out, ex, all_bits, bit = 1, mask, j;
            unsigned long res_in = 0, res_out = 0, res_ex = 0;
            const struct file_operations *f_op = NULL;
            struct file *file = NULL;
             // 先取出當前循環週期中的32(設long佔32位)個文件描述符對應的bitmaps
            in = *inp++; out = *outp++; ex = *exp++;
            all_bits = in | out | ex;// 組合一下,有的fd可能只監測讀,或者寫,或者err,或者同時都監測
            if (all_bits == 0) {
                i += __NFDBITS; //如果這個字沒有待查找的描述符, 跳過這個長字(32位,__NFDBITS=32),取下一個32個fd的循環中
                continue;

            }
            // 本次32個fd的循環中有需要監測的狀態存在
            for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
                int fput_needed;
                if (i >= n)
                   break;
                if (!(bit & all_bits)) // bit每次循環後左移一位的作用在這裏,用來跳過沒有狀態監測的fd
                   continue;

                file = fget_light(i, &fput_needed);//得到file結構指針,並增加引用計數字段f_count
                if (file) {// 如果file存在(這個文件描述符對應的文件確實打開了)
                    f_op = file->f_op;
                    mask = DEFAULT_POLLMASK;
                    if (f_op && f_op->poll) //這個文件對應的驅動程序提供了poll函數(fop->poll)。
                        mask = (*f_op->poll)(file, retval ? NULL : wait);//調用驅動程序中的poll函數。
                    /*  調用驅動程序中的poll函數,以evdev驅動中的evdev_poll()爲例
                     *  該函數會調用函數poll_wait(file, &evdev->wait, wait),
                     *  繼續調用__pollwait()回調來分配一個poll_table_entry結構體,
                     *  該結構體有一個內嵌的等待隊列項,
                     *  設置好wake時調用的回調函數後將其添加到驅動程序中的等待隊列頭中。  */

                    fput_light(file, fput_needed);  // 釋放file結構指針,實際就是減小他的一個引用計數字段f_count。
                    //記錄結果。poll函數返回的mask是設備的狀態掩碼。
                    if ((mask & POLLIN_SET) && (in & bit)) {
                        res_in |= bit; //如果是這個描述符可讀, 將這個位置位
                        retval++;   //返回描述符個數加1
                    }

                    if ((mask & POLLOUT_SET) && (out & bit)) {
                        res_out |= bit;
                        retval++;
                    }

                    if ((mask & POLLEX_SET) && (ex & bit)) {
                        res_ex |= bit;
                        retval++;
                    }
                }
                /*
                 *  cond_resched()將判斷是否有進程需要搶佔當前進程,
                 *  如果是將立即發生調度,這只是爲了增加強佔點。
                 *  (給其他緊急進程一個機會去執行,增加了實時性)
                 *  在支持搶佔式調度的內核中(定義了CONFIG_PREEMPT),
                 *  cond_resched是空操作。
                 */
                cond_resched();
            }

            //返回結果
            if (res_in)
                *rinp = res_in;
            if (res_out)
                *routp = res_out;
            if (res_ex)
               *rexp = res_ex;
        }
        wait = NULL;
        if (retval || !*timeout || signal_pending(current)) // signal_pending(current)檢查當前進程是否有信號要處理
            break;
        if(table.error) {
            retval = table.error;
            break;
        }
 
        if (*timeout < 0) {
            /* Wait indefinitely 無限期等待*/
            __timeout = MAX_SCHEDULE_TIMEOUT;
        } elseif (unlikely(*timeout >= (s64)MAX_SCHEDULE_TIMEOUT - 1)) {
            /* Wait for longer than MAX_SCHEDULE_TIMEOUT. Do it in a loop */
            __timeout = MAX_SCHEDULE_TIMEOUT - 1;
            *timeout -= __timeout;
        } else {
            __timeout = *timeout;
            *timeout = 0;
       }

         /* schedule_timeout 用來讓出CPU;
          * 在指定的時間用完以後或者其它事件到達並喚醒進程(比如接收了一個信號量)時,
          * 該進程纔可以繼續運行  */
        __timeout = schedule_timeout(__timeout);
        if (*timeout >= 0)
            *timeout += __timeout;
    }
    __set_current_state(TASK_RUNNING);

    poll_freewait(&table);
    return retval;
}

 

      源碼中比較重要的結構體有四個:

      struct poll_wqueues、struct poll_table_page、struct poll_table_entry、struct poll_table_struct。

      每一個調用select()系統調用的應用進程都會存在一個struct poll_wqueues結構體,用來統一輔佐實現這個進程中所有待監測的fd的輪詢工作,後面所有的工作和都這個結構體有關,所以它非常重要。

struct poll_wqueues {
       poll_table pt;
       struct poll_table_page *table;
       struct task_struct *polling_task; //保存當前調用select的用戶進程struct task_struct結構體
       int triggered;            // 當前用戶進程被喚醒後置成1,以免該進程接着進睡眠
       int error;                 // 錯誤碼
       int inline_index;        // 數組inline_entries的引用下標
       struct poll_table_entry inline_entries[N_INLINE_POLL_ENTRIES];
};

      實際上結構體poll_wqueues內嵌的poll_table_entry數組inline_entries[] 的大小是有限的,如果空間不夠用,後續會動態申請物理內存頁以鏈表的形式掛載poll_wqueues.table上統一管理。接下來的兩個結構體就和這項內容密切相關:

struct poll_table_page { // 申請的物理頁都會將起始地址強制轉換成該結構體指針
       struct poll_table_page   *next;      // 指向下一個申請的物理頁
       struct poll_table_entry  *entry;     // 指向entries[]中首個待分配(空的) poll_table_entry地址
       struct poll_table_entry  entries[0]; // 該page頁後面剩餘的空間都是待分配的poll_table_entry結構體
};

      對每一個fd調用fop->poll() => poll_wait() => __pollwait()都會先從poll_wqueues.inline_entries[]中分配一個poll_table_entry結構體,直到該數組用完纔會分配物理頁掛在鏈表指針poll_wqueues.table上然後纔會分配一個poll_table_entry結構體(poll_get_entry函數)。

      poll_table_entry具體用處:函數__pollwait聲明如下:

static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p);

      該函數調用時需要3個參數,第一個是特定fd對應的file結構體指針,第二個就是特定fd對應的硬件驅動程序中的等待隊列頭指針,第3個是調用select()的應用進程中poll_wqueues結構體的poll_table項(該進程監測的所有fd調用fop->poll函數都用這一個poll_table結構體)。

struct poll_table_entry {
       struct file     *filp;                 // 指向特定fd對應的file結構體;
       unsigned long   key;                   // 等待特定fd對應硬件設備的事件掩碼,如POLLIN、 POLLOUT、POLLERR;
       wait_queue_t    wait;                  // 代表調用select()的應用進程,等待在fd對應設備的特定事件 (讀或者寫)的等待隊列頭上,的等待隊列項;
       wait_queue_head_t   *wait_address;     // 設備驅動程序中特定事件的等待隊列頭(該fd執行fop->poll,需要等待時在哪等,所以叫等待地址);
};

總結:

1.  特定的硬件設備驅動程序的事件等待隊列頭是有限個數的,通常是有讀事件和寫事件的等待隊列頭;

2.  而一個調用了select()的應用進程只存在一個poll_wqueues結構體;

3.  該應用程序可以有多個fd在進行同時監測其各自的事件發生,但該應用進程中每一個fd有多少個poll_table_entry存在,那就取決於fd對應的驅動程序中有幾個事件等待隊列頭了,也就是說,通常驅動程序的poll函數中需要對每一個事件的等待隊列頭調用poll_wait()函數。比如,如果有讀寫兩個等待隊列頭,那麼就在這個應用進程中存在兩個poll_table_entry結構體,在這兩個事件的等待隊列頭中分別將兩個等待隊列項加入;

4.  如果有多個應用進程使用select()方式同時在訪問同一個硬件設備,此時硬件驅動程序中加入等待隊列頭中的等待隊列項對每一個應用程序來說都是相同數量的(一個事件等待隊列頭一個,數量取決於事件等待隊列頭的個數)。

      do_select函數中,遍歷所有n個fd,對每一個fd調用對應驅動程序中的poll函數。

      驅動程序中poll一般具有如下形式:

static unsigned int XXX_poll(struct file *filp, poll_table *wait)
{
   unsigned int mask = 0;
   struct XXX_dev *dev = filp->private_data;
   ...
   poll_wait(filp, &dev->r_wait, wait);
   poll_wait(filp ,&dev->w_wait, wait);
   if(CAN_READ)//讀就緒
   {
      mask |= POLLIN | POLLRDNORM;
   }
   if(CAN_WRITE)//寫就緒
   {
      mask |= POLLOUT | POLLRDNORM;
   }
   ...
   return mask;
}

       以字符設備evdev爲例(文件drivers/input/evdev.c)

static unsigned int evdev_poll(struct file *file, poll_table *wait)
{
       struct evdev_client *client = file->private_data;
       struct evdev *evdev = client->evdev;
       poll_wait(file, &evdev->wait, wait);
       return ((client->head == client->tail) ? 0 : (POLLIN | POLLRDNORM)) | (evdev->exist ? 0 : (POLLHUP | POLLERR));
}

static inline void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p)
{
       if (p && wait_address)
              p->qproc(filp, wait_address, p);
}

      其中wait_address是驅動程序需要提供的等待隊列頭,來容納後續等待該硬件設備就緒的進程對應的等待隊列項。

typedef void (*poll_queue_proc)(struct file *, wait_queue_head_t *, struct poll_table_struct *);

typedef struct poll_table_struct {
       poll_queue_proc qproc;
       unsigned long key;
} poll_table;

      fop->poll()函數的poll_table參數是從哪裏傳進來的?好生閱讀過代碼就可以發現,do_select()函數中存在一個結構體structpoll_wqueues,其內嵌了一個poll_table的結構體,所以在後面的大循環中依次調用各個fd的fop->poll()傳遞的poll_table參數都是poll_wqueues.poll_table。

      poll_table結構體的定義其實蠻簡單,就一個函數指針,一個key值。這個函數指針在整個select過程中一直不變,而key則會根據不同的fd的監測要求而變化。

      qproc函數初始化在函數do_select() –> poll_initwait() -> init_poll_funcptr(&pwq->pt, __pollwait)中實現,回調函數就是__pollwait()。

int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
       struct poll_wqueues table;
       ...
       poll_initwait(&table);
       ...
}

void poll_initwait(struct poll_wqueues *pwq)
{
       init_poll_funcptr(&pwq->pt, __pollwait);
       ...
}

static inline void init_poll_funcptr(poll_table *pt, poll_queue_proc qproc)
{
       pt->qproc = qproc;
       pt->key   = ~0UL; /* all events enabled */
}

/* Add a new entry */
static void __pollwait(struct file *filp, wait_queue_head_t *wait_address, poll_table *p)
{
   struct poll_wqueues *pwq = container_of(p, struct poll_wqueues, pt);
   struct poll_table_entry *entry = poll_get_entry(pwq);
   if (!entry)
      return;
   get_file(filp);
   entry->filp = filp;      // 保存對應的file結構體
   entry->wait_address = wait_address;  // 保存來自設備驅動程序的等待隊列頭
   entry->key = p->key;  // 保存對該fd關心的事件掩碼
   init_waitqueue_func_entry(&entry->wait, pollwake);// 初始化等待隊列項,pollwake是喚醒該等待隊列項時候調用的函數
   entry->wait.private = pwq; // 將poll_wqueues作爲該等待隊列項的私有數據,後面使用
   add_wait_queue(wait_address, &entry->wait);// 將該等待隊列項添加到從驅動程序中傳遞過來的等待隊列頭中去。
}

     驅動程序在得知設備有IO事件時(通常是該設備上IO事件中斷),會調用wakeup,wakeup –> __wake_up_common -> curr->func(即pollwake)。

static int pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
       struct poll_table_entry *entry;
       entry = container_of(wait, struct poll_table_entry, wait);// 取得poll_table_entry結構體指針
       if (key && !((unsigned long)key & entry->key))/*這裏的條件判斷至關重要,避免應用進程被誤喚醒,什麼意思?*/
              return 0;
       return __pollwake(wait, mode, sync, key);
}

        pollwake調用__pollwake,最終調用default_wake_function。

static int __pollwake(wait_queue_t *wait, unsigned mode, int sync, void *key)
{
   struct poll_wqueues *pwq = wait->private;
   DECLARE_WAITQUEUE(dummy_wait, pwq->polling_task);
   smp_wmb();
   pwq->triggered = 1; // select()用戶進程只要有被喚醒過,就不可能再次進入睡眠,因爲這個標誌在睡眠的時候有用
   return default_wake_function(&dummy_wait, mode, sync, key); // 默認通用的喚醒函數
}

      最終喚醒調用select的進程,在do_select函數的schedule_timeout函數之後繼續執行(繼續for(;;),也即從新檢查每一個fd是否有事件發生),此次檢查會發現設備的該IO事件,於是select返回用戶層。

       綜上給出兩張圖便於理解:

            

             

                                               (此處來源於網絡,可以放大觀看)

字符設備驅動

      上面詳細分析了select在內核中的實現原理,可以發起其中最關鍵的一步就是select循環會遍歷要監聽的文件的poll函數,詢問文件當前資源的就緒狀態。那麼這個poll函數時在哪裏定義或者聲明的呢?沒錯,就是設備驅動,具體的講是字符設備驅動(不支持隨機訪問,只能以字節流順序訪問)。在linux中一切皆文件,無論是一個鍵盤還是一個鼠標設備,它們在linux中最終都是以一個設備文件的形式存在(dev目錄下),要想操作這些設備,無非就是對設備進行讀寫操作,那麼依然可以像使用一個普通文件一樣,使用標準的系統庫函數對其進行操作。

      設備驅動是內核與硬件之間的紐帶,一個硬件設備要想被映射爲一個設備文件就離不開設備驅動支持,通常,一個字符設備驅動程序就是一個實現了某些內核接口的內核模塊,這個接口在內核中就是file_operations:             

struct file_operations {
	struct module *owner;
	loff_t (*llseek) (struct file *, loff_t, int);
	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
	ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
	ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
	int (*iterate) (struct file *, struct dir_context *);
	int (*iterate_shared) (struct file *, struct dir_context *);
    //關注此接口
	unsigned int (*poll) (struct file *, struct poll_table_struct *);
	long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
	long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
	int (*mmap) (struct file *, struct vm_area_struct *);
	int (*open) (struct inode *, struct file *);
	int (*flush) (struct file *, fl_owner_t id);
	int (*release) (struct inode *, struct file *);
	int (*fsync) (struct file *, loff_t, loff_t, int datasync);
	int (*aio_fsync) (struct kiocb *, int datasync);
	int (*fasync) (int, struct file *, int);
	int (*lock) (struct file *, int, struct file_lock *);
	ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
	unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
	int (*check_flags)(int);
	int (*flock) (struct file *, int, struct file_lock *);
	ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
	ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
	int (*setlease)(struct file *, long, struct file_lock **, void **);
	long (*fallocate)(struct file *file, int mode, loff_t offset,
			  loff_t len);
	void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
	unsigned (*mmap_capabilities)(struct file *);
#endif
	ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
			loff_t, size_t, unsigned int);
	int (*clone_file_range)(struct file *, loff_t, struct file *, loff_t,
			u64);
	ssize_t (*dedupe_file_range)(struct file *, u64, u64, struct file *,
			u64);
};

      可以看到,file_operations中定義了很多熟悉的文件操作(read、write、flush等),也包括select使用的poll接口。由於本文主要驗證select機制,因此驅動程序會主要關注poll接口的實現。

      由於操作具體硬件相對比較麻煩(目前不屬於本文重點),因此本文用內存來模擬一個字符設備,讓其實現poll接口從而可以支持select機制,下面就是源碼。

內存字符設備驅動模擬

   文件memdev.h:

#ifndef _MEMDEV_H_
#define _MEMDEV_H_

#ifndef MEMDEV_MAJOR
#define MEMDEV_MAJOR 0   /*預設的mem的主設備號*/
#endif

#ifndef MEMDEV_NR_DEVS
#define MEMDEV_NR_DEVS 2    /*設備數*/
#endif

#ifndef MEMDEV_SIZE
#define MEMDEV_SIZE 4096
#endif

/*mem設備描述結構體*/
struct mem_dev                                     
{                                                        
  char *data;                      
  unsigned long size; 
  wait_queue_head_t inq;  
};

#endif /* _MEMDEV_H_ */

   文件memdev.c:

#include <linux/module.h>
#include <linux/types.h>
#include <linux/fs.h>
#include <linux/errno.h>
#include <linux/mm.h>
#include <linux/sched.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <asm/io.h>
#include <asm/system.h>
#include <asm/uaccess.h>

#include <linux/poll.h>
#include "memdev.h"

static mem_major = MEMDEV_MAJOR;
bool have_data = false; /*表明設備有足夠數據可供讀*/

module_param(mem_major, int, S_IRUGO);

struct mem_dev *mem_devp; /*設備結構體指針*/

struct cdev cdev; 

/*文件打開函數*/
int mem_open(struct inode *inode, struct file *filp)
{
    struct mem_dev *dev;
    
    /*獲取次設備號*/
    int num = MINOR(inode->i_rdev);

    if (num >= MEMDEV_NR_DEVS) 
            return -ENODEV;
    dev = &mem_devp[num];
    
    /*將設備描述結構指針賦值給文件私有數據指針*/
    filp->private_data = dev;
    
    return 0; 
}

/*文件釋放函數*/
int mem_release(struct inode *inode, struct file *filp)
{
  return 0;
}

/*讀函數*/
static ssize_t mem_read(struct file *filp, char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  struct mem_dev *dev = filp->private_data; /*獲得設備結構體指針*/

  /*判斷讀位置是否有效*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count > MEMDEV_SIZE - p)
    count = MEMDEV_SIZE - p;
    
  while (!have_data) /* 沒有數據可讀,考慮爲什麼不用if,而用while */
  {
        if (filp->f_flags & O_NONBLOCK)
            return -EAGAIN;
    
    wait_event_interruptible(dev->inq,have_data);
  }

  /*讀數據到用戶空間*/
  if (copy_to_user(buf, (void*)(dev->data + p), count))
  {
    ret =  - EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;
   
    printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
  }
  
  have_data = false; /* 表明不再有數據可讀 */
  /* 喚醒寫進程 */
  return ret;
}

/*寫函數*/
static ssize_t mem_write(struct file *filp, const char __user *buf, size_t size, loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  struct mem_dev *dev = filp->private_data; /*獲得設備結構體指針*/
  
  /*分析和獲取有效的寫長度*/
  if (p >= MEMDEV_SIZE)
    return 0;
  if (count > MEMDEV_SIZE - p)
    count = MEMDEV_SIZE - p;

  /*從用戶空間寫入數據*/
  if (copy_from_user(dev->data + p, buf, count))
    ret =  - EFAULT;
  else
  {
    *ppos += count;
    ret = count;
    
    printk(KERN_INFO "written %d bytes(s) from %d\n", count, p);
  }
  
  have_data = true; /* 有新的數據可讀 */
    
    /* 喚醒讀進程 */
    wake_up(&(dev->inq));

  return ret;
}

/* seek文件定位函數 */
static loff_t mem_llseek(struct file *filp, loff_t offset, int whence)
{ 
    loff_t newpos;

    switch(whence) {
      case 0: /* SEEK_SET */
        newpos = offset;
        break;

      case 1: /* SEEK_CUR */
        newpos = filp->f_pos + offset;
        break;

      case 2: /* SEEK_END */
        newpos = MEMDEV_SIZE -1 + offset;
        break;

      default: /* can't happen */
        return -EINVAL;
    }
    if ((newpos<0) || (newpos>MEMDEV_SIZE))
        return -EINVAL;
        
    filp->f_pos = newpos;
    return newpos;

}
unsigned int mem_poll(struct file *filp, poll_table *wait)
{
    struct mem_dev  *dev = filp->private_data; 
    unsigned int mask = 0;
    
   /*將等待隊列添加到poll_table */
    poll_wait(filp, &dev->inq,  wait);
 
    
    if (have_data)         mask |= POLLIN | POLLRDNORM;  /* readable */

    return mask;
}


/*文件操作結構體*/
static const struct file_operations mem_fops =
{
  .owner = THIS_MODULE,
  .llseek = mem_llseek,
  .read = mem_read,
  .write = mem_write,
  .open = mem_open,
  .release = mem_release,
  .poll = mem_poll,
};

/*設備驅動模塊加載函數*/
static int memdev_init(void)
{
  int result;
  int i;

  dev_t devno = MKDEV(mem_major, 0);

  /* 靜態申請設備號*/
  if (mem_major)
    result = register_chrdev_region(devno, 2, "memdev");
  else  /* 動態分配設備號 */
  {
    result = alloc_chrdev_region(&devno, 0, 2, "memdev");
    mem_major = MAJOR(devno);
  }  
  
  if (result < 0)
    return result;

  /*初始化cdev結構*/
  cdev_init(&cdev, &mem_fops);
  cdev.owner = THIS_MODULE;
  cdev.ops = &mem_fops;
  
  /* 註冊字符設備 */
  cdev_add(&cdev, MKDEV(mem_major, 0), MEMDEV_NR_DEVS);
   
  /* 爲設備描述結構分配內存*/
  mem_devp = kmalloc(MEMDEV_NR_DEVS * sizeof(struct mem_dev), GFP_KERNEL);
  if (!mem_devp)    /*申請失敗*/
  {
    result =  - ENOMEM;
    goto fail_malloc;
  }
  memset(mem_devp, 0, sizeof(struct mem_dev));
  
  /*爲設備分配內存*/
  for (i=0; i < MEMDEV_NR_DEVS; i++) 
  {
        mem_devp[i].size = MEMDEV_SIZE;
        mem_devp[i].data = kmalloc(MEMDEV_SIZE, GFP_KERNEL);
        memset(mem_devp[i].data, 0, MEMDEV_SIZE);
  
      /*初始化等待隊列*/
     init_waitqueue_head(&(mem_devp[i].inq));
     //init_waitqueue_head(&(mem_devp[i].outq));
  }
   
  return 0;

  fail_malloc: 
  unregister_chrdev_region(devno, 1);
  
  return result;
}

/*模塊卸載函數*/
static void memdev_exit(void)
{
  cdev_del(&cdev);   /*註銷設備*/
  kfree(mem_devp);     /*釋放設備結構體內存*/
  unregister_chrdev_region(MKDEV(mem_major, 0), 2); /*釋放設備號*/
}

MODULE_AUTHOR("David Xie");
MODULE_LICENSE("GPL");

module_init(memdev_init);
module_exit(memdev_exit);

      把上面的驅動編譯爲內核模塊並掛載到內核中,先寫一個寫內存設備的一個月程序,如下:

#include <stdio.h>

int main()
{
    FILE *fp = NULL;
    char Buf[128];
    
    
    /*打開設備文件*/
    fp = fopen("/dev/memdev0","r+");
    if (fp == NULL)
    {
        printf("Open Dev memdev Error!\n");
        return -1;
    }
    
    /*寫入設備*/
    strcpy(Buf,"memdev is char dev!");
    printf("Write BUF: %s\n",Buf);
    fwrite(Buf, sizeof(Buf), 1, fp);
    
    sleep(5);
    fclose(fp);
    
    return 0;    

}

     再寫一個用select機制讀取內存設備的應用程序,如下:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <errno.h>

int main()
{
    int fd;
    fd_set rds;
    int ret;
    char Buf[128];
    
    /*初始化Buf*/
    strcpy(Buf,"memdev is char dev!");
    printf("BUF: %s\n",Buf);
    
    /*打開設備文件*/
    fd = open("/dev/memdev0",O_RDWR);
    
    FD_ZERO(&rds);
    FD_SET(fd, &rds);

    /*清除Buf*/
    strcpy(Buf,"Buf is NULL!");
    printf("Read BUF1: %s\n",Buf);

    ret = select(fd + 1, &rds, NULL, NULL, NULL);
    if (ret < 0) 
    {
        printf("select error!\n");
        exit(1);
    }
    if (FD_ISSET(fd, &rds)) 
        read(fd, Buf, sizeof(Buf));            
    
    /*檢測結果*/
    printf("Read BUF2: %s\n",Buf);
    
    close(fd);
    
    return 0;    
}

相關文章

原從glibc源碼看系統調用原理

 

參考資料

http://www.cnblogs.com/apprentice89/archive/2013/05/09/3070051.html

http://www.cnblogs.com/apprentice89/archive/2013/05/09/3064975.html

 

© 著作權歸作者所有

打賞點贊 (4)收藏 (84)

 分享

打印 舉報

上一篇:linux內核之模塊moudle_init與moudle_exit原理

下一篇:從glibc源碼看系統調用原理

黑客畫家

黑客畫家

粉絲 174

 

博文 242

 

碼字總數 620228

 

作品 0

 杭州

 

 高級程序員

關注 私信 提問

 

評論(10)

sparkliu

sparkliu 

2017/06/21 21:53

爲什麼代碼看不全,少了一部分

 舉報

久永

久永 

2017/06/05 10:08

引用來自“久永”的評論

請能在開始給外行一兩句簡單的介紹好嗎?一下子就深入。。。很疼的。。。

引用來自“申延剛”的評論

外行的話得買本操作系統先看看

引用來自“久永”的評論

操作系統早看過了,計算機領域專業術語那麼多,你總的提示下是那個方面的把?
比如 select ,SQL裏面有,LINQ 裏面有,我突然文章裏面開始就說“select”原理,你知道是啥?

引用來自“申延剛”的評論

select多路複用, 這個是標題中提到的一個短語。

1,懂系統的一看就知道這個不是 SQL中的select
2,博主的觀點已經十分明確了,我有好的提示你可以去看一下操作系統的書,但是你不應該非常不友好的反噴我
3,你根本不想弄清楚博主的內容,你只是想在吵架中獲得勝利
4,你已經贏了,所以不要回復我的留言,謝謝1,你這個條件根本不成立,成立的條件是“只搞系統”或者起碼是“當前在搞系統”——而且請把系統補全爲“操作系統”,因爲軟件行業“系統”多如牛毛!
2,讓所有人看看,第一句我調侃的語氣給作者建議,請問哪裏噴了?倒是某人一上來就來噴我!
3,重複上一條,我是給作者建議,從來沒想到和什麼無聊的人辯論。作者聽不聽都是他的權利,但是還輪不到別人JJWW。
4,我贏了不是該對方閉嘴嗎?“輸了”要服氣,“輸了”還要拿戰利品?“請不要當別人傻瓜”。
+1,好好工作,年輕人!當你的想法成熟了再提出來,而不是不斷的拋出自己的想法觀點,讓領導、老闆幫你試錯,人家不是你什麼人。

 舉報

久永

久永 

2017/06/05 10:00

引用來自“申延剛”的評論

 

引用來自“久永”的評論

請能在開始給外行一兩句簡單的介紹好嗎?一下子就深入。。。很疼的。。。

引用來自“申延剛”的評論

外行的話得買本操作系統先看看

引用來自“久永”的評論

操作系統早看過了,計算機領域專業術語那麼多,你總的提示下是那個方面的把?
比如 select ,SQL裏面有,LINQ 裏面有,我突然文章裏面開始就說“select”原理,你知道是啥?select多路複用, 這個是標題中提到的一個短語。

1,懂系統的一看就知道這個不是 SQL中的select
2,博主的觀點已經十分明確了,我有好的提示你可以去看一下操作系統的書,但是你不應該非常不友好的反噴我
3,你根本不想弄清楚博主的內容,你只是想在吵架中獲得勝利
4,你已經贏了,所以不要回復我的留言,謝謝

 

回覆@申延剛 : 對不起,我就是因爲自己看過,所以後來才“恍然大悟”原來說的是這個!否則,如果是個沒有相關知識的,肯定早就不知所云的關掉了,誰還跟你討論?有句話說的是這同樣的,“請不要把自己的用戶當白癡”,因爲你們絕大多數在一個級別。

 舉報

申延剛

申延剛 

2017/06/05 09:37

引用來自“久永”的評論

請能在開始給外行一兩句簡單的介紹好嗎?一下子就深入。。。很疼的。。。

引用來自“申延剛”的評論

外行的話得買本操作系統先看看

引用來自“久永”的評論

操作系統早看過了,計算機領域專業術語那麼多,你總的提示下是那個方面的把?
比如 select ,SQL裏面有,LINQ 裏面有,我突然文章裏面開始就說“select”原理,你知道是啥?select多路複用, 這個是標題中提到的一個短語。

1,懂系統的一看就知道這個不是 SQL中的select
2,博主的觀點已經十分明確了,我有好的提示你可以去看一下操作系統的書,但是你不應該非常不友好的反噴我
3,你根本不想弄清楚博主的內容,你只是想在吵架中獲得勝利
4,你已經贏了,所以不要回復我的留言,謝謝

 舉報

久永

久永 

2017/06/01 10:09

引用來自“久永”的評論

請能在開始給外行一兩句簡單的介紹好嗎?一下子就深入。。。很疼的。。。

引用來自“申延剛”的評論

外行的話得買本操作系統先看看操作系統早看過了,計算機領域專業術語那麼多,你總的提示下是那個方面的把?
比如 select ,SQL裏面有,LINQ 裏面有,我突然文章裏面開始就說“select”原理,你知道是啥?

 舉報

申延剛

申延剛 

2017/06/01 08:36

引用來自“久永”的評論

請能在開始給外行一兩句簡單的介紹好嗎?一下子就深入。。。很疼的。。。外行的話得買本操作系統先看看

 舉報

久永

久永 

2017/05/31 19:45

請能在開始給外行一兩句簡單的介紹好嗎?一下子就深入。。。很疼的。。。

 舉報

黑客畫家

黑客畫家 博主 

2017/05/31 10:32

引用來自“SimpleStupid”的評論

上首頁都這麼沒落,不知道是大家去過端午了,還是沒人玩C了不是沒人玩C,而是一般人都不會關心內核層面的實現機制

 舉報

滄海一刀

滄海一刀 

2017/05/31 08:18

非常感謝,最近一直在找select原理的相關文章

 舉報

S

SimpleStupid 

2017/05/30 22:10

上首頁都這麼沒落,不知道是大家去過端午了,還是沒人玩C了

 舉報

相關文章最新文章

IO多路複用原理剖析

(最近筆試遇到筆試題:select,poll,epoll都是IO多路複用的機制)。 I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應...

Panda_Jerry

 

2017/11/01

 

 0

 

 0

解讀I/O多路複用技術

前言 當我們要編寫一個echo服務器程序的時候,需要對用戶從標準輸入鍵入的交互命令做出響應。在這種情況下,服務器必須響應兩個相互獨立的I/O事件:1)網絡客戶端發起網絡連接請求,2)用戶在...

新棟BOOK

 

2017/11/19

 

 0

 

 0

一文讓你讀懂懂阻塞、非阻塞、同步、異步IO

介紹 在談及網絡IO的時候總避不開阻塞、非阻塞、同步、異步、IO多路複用、select、poll、epoll等這幾個詞語。在面試的時候也會被經常問到這幾個的區別。本文就來講一下這幾個詞語的含義、區別...

lemonwater

 

2018/05/16

 

 663

 

 1

Netty精粹之JAVA NIO開發需要知道的

學習Netty框架以及相關源碼也有一小段時間了,恰逢今天除夕,寫篇文章總結一下。Netty是個高效的JAVA NIO框架,總體框架基於異步非阻塞的設計,基於網絡IO事件驅動,主要貢獻在於可以讓用戶基...

Float_Luuu

 

2016/02/07

 

 14.9K

 

 3

Java NIO 機制分析(一) Java IO的演進

一、引言 Java1.4之前的早期版本,Java對I/O的支持並不完善,開發人員再開發高性能I/O程序的時候,會面臨一些巨大的挑戰和困難,主要有以下一些問題: (1)沒有數據緩衝區,I/O性能存在問題...

宸明

 

2018/04/20

 

 102

 

 0

加載更多

 

OSCHINA 社區

關於我們聯繫我們合作伙伴Open API

在線工具

碼雲 Gitee.com企業研發管理CopyCat-代碼克隆檢測實用在線工具

微信公衆號

微信公衆號

OSCHINA APP

聚合全網技術文章,根據你的閱讀喜好進行個性推薦

下載 APP

©OSCHINA(OSChina.NET)

 

工信部

 開源軟件推進聯盟 

指定官方社區

深圳市奧思網絡科技有限公司版權所有

 粵ICP備12009483號-3

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章