深入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

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