[轉]基礎 Linux Device Driver 驅動程式#10 (select/poll)

備份好文章
http://csw-dawn.blogspot.tw/2012/01/linux-device-driver-10-selectpoll.html


基礎 Linux Device Driver 驅動程式#10 (select/poll)

相信各位都有在Linux上寫程式的經驗,
當您程式裡呼叫 open 時,Linux 預設會以 blocking mode的方式開啟,
block 是指當 process 為了等待某件事的發生,而進入 sleep 狀態的情形。

像 read 就是其中一種,當沒資料讀取時,process 就會被 block。
write 也是一樣,在寫入資料時,寫入對象還無法處理資料時,一樣會 block。

對某些程式來說,如果 read 系統呼叫被 block 的話,有時就會有設計上的問題,
所以為了避免這種問題發生,Linux 就準備了以下方式。

1. Non-blocking 模式

啟用 non-blocking 模式後,不管是 read 還是 write 就不會被 block住。但會傳回 errno 錯誤碼,
這時就必須自已再做讀寫的動作。
想使用 non-blocking 模式的話,可在 open() 開檔時指定 O_NONBLOCK。

2. 同時執行多個同步 I/O 工作

同時執行多個同步 I/O 工作 指的是使用 select 系統呼叫的做法。
select 系統呼叫本身會被 block, 但可指定 timeout。

使用 select() 的時候,有幾個常用的函式巨集,可參考 man 2 select。

int select(int nfds, fd_set *readfds, fd_set *writefds,
    fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

還有一個和 select()很類似的 poll()系統呼叫,以基本功能來說,select()與poll()都一樣,
但指定的 file handler的方式不一樣,且指定多個 file handler 的時候,poll()走訪所有
file handler的速度比較快。 可參考 man 2 poll。

int poll(struct pollfd *fds, nfds_t nfds, int timeout);

應用程式想同時執行多個同步 I/O 工作時,可使用的系統呼叫有好幾個,
但在驅動程式,只需要準備一個函式即可。
不管 user process 用了哪個系統呼叫,kernel 都只會呼叫驅動程式提供的這個函式。

Character 類型的裝置想支援同時執行多個同步 I/O 工作的話,只要在驅動程式準備 poll方法即可。
poll方法會收到 file_operations 結構。

unsigned int (*poll) (struct file *, struct poll_table_struct *);

poll方法會在kernel 處理 select 與 poll 之類的系統呼叫時用到。它必須的執行工作如下:
1. 在 wait queue 豋記。
2. 傳回目前可以操作的狀態。

在呼叫同時執行多個同步 I/O 工作的系統呼叫時,block 直到狀態變化的動作,指的是在 kernel裡面 sleep。
如果要 sleep的話,要先準備 wait queue(wait_queue_head_t),這個由驅動程式負責提供。
豋記 wait queue 的工作可透過 poll_wait()完成,它定義在 include/linux/poll.h

void poll_wait(struct file * filp, wait_queue_head_t * wait_address, poll_table *p);
// filp:執行操作的設備文件指針
// wait_address:設備驅動的等待隊列頭
// p:內核傳遞的輪詢表指針
 調用poll_wait()函數將等待隊列添加到poll_table輪詢表 (出處參考)

在呼叫同時執行多個同步 I/O 工作的系統呼叫時,解除 block 的時機,是驅動程式透過傳給 poll_wait()
的 wait queue 被喚醒的時候。Kernel 在被 wait queue 喚醒之後,會再次呼叫驅動程式的 poll()
確認是否成為等待中的狀態(可寫入或可讀出),如果是的話,FD_ISSET 巨集就會成立。
想判斷是不是這種狀態的話,還是需要驅動程式提供資訊才行,這個資訊就是透過poll()方法的傳回值來表示。
傳回值要透過 include/linux/poll.h 定義的巨集 OR 起來表示,下面是常用到的組合。
數值定義於 android/bionic/libc/kernel/arch-arm/asm/poll.h

POLLIN|POLLRDNORM                     可讀取
POLLOUT|POLLWRNORM                    可寫入
POLLIN|POLLRDNORM|POLLOUT|POLLWRNORM  可讀寫
POLLERR                               發生錯誤
POLLHUP                               裝置離線(EOF)

說了那麼多,還倒不如寫個程式比較好了解。

poll操作一般結構:

xxx_poll() 
{ 
  unsigned int mask = 0;//定義返回值 
  ... 
  //調用poll_wait()把進程添加到輪詢表 
  ... 
  if(device is ready for read){ 
    mask = POLLIN | POLLRDNORM; 
  } 
  ... 
  return mask; 
} 

剛試了一下,果然新舊版核心還是有差,幾個問題,稍為提出來討論一下:
1. 要用 kmalloc 及 kfree 的話,要 include 註:其實在上一個示範程式就已有這問題了。
請參考: 基礎 Linux Device Driver 驅動程式#9 (IOCTL)
2. void init_MUTEX (struct semaphore *sem); /* 新版 kernel 已不適用 */
改用
void sema_init (struct semaphore *sem, int val);

test_select.c 原始碼如下:

/*****************************************************************************/
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define DRIVER_NAME "test_select"

static unsigned int test_select_major = 0;
static unsigned int num_of_dev = 1;
static struct cdev test_select_cdev;
static unsigned int timeout_value = 10;

struct test_select_data {
 struct timer_list timeout;
 spinlock_t lock;
 wait_queue_head_t read_wait;
 int timeout_done;
 struct semaphore sem;
};

unsigned int test_select_poll(struct file *filp, poll_table *wait)
{
 struct test_select_data *data = filp->private_data;
 unsigned int mask = POLLOUT|POLLWRNORM;
 printk(KERN_ALERT "Call test_select_poll.\n");

 if (data == NULL)
  return -EBADFD;
 down(&data->sem);
 poll_wait(filp, &data->read_wait, wait);

 if (data->timeout_done == 1) {    /* readable */
  mask |= POLLIN|POLLRDNORM;
 }
 up(&data->sem);
 printk(KERN_ALERT "%s returned (mask 0x%x)\n", __func__,  mask);
}

static void test_select_timeout(unsigned long arg)
{
 struct test_select_data *data = (struct test_select_data*)arg;
 unsigned long flags;
 printk(KERN_ALERT "Call test_select_timeout.\n");

 spin_lock_irqsave(&data->lock, flags);

 data->timeout_done = 1;
 wake_up_interruptible(&data->read_wait);

 spin_unlock_irqrestore(&data->lock, flags);
}

ssize_t test_select_write (struct file *filp, const char __user *buf, size_t count, loff_t *pos)
{
 return -EFAULT;
}

ssize_t test_select_read(struct file *filp, char __user *buf, size_t count, loff_t *f_pos)
{
 struct test_select_data *data = filp->private_data;
 int i;
 unsigned char val;
 int retval;

 if (down_interruptible(&data->sem))
  return -ERESTARTSYS;
 if (data->timeout_done == 0) {    /* no read */
  up(&data->sem);
  if (filp->f_flags & O_NONBLOCK)    /* non-blocking mode */
   return -EAGAIN;
  do {
   retval = wait_event_interruptible_timeout(
       data->read_wait,
       data->timeout_done == 1,
       1*HZ);
   if (retval == -ERESTARTSYS)
    return -ERESTARTSYS;
  } while (retval == 0);    /* timeout elapsed */

  if (down_interruptible(&data->sem))
   return -ERESTARTSYS;
 }
 val = 0xff;
 for (i = 0; i < count; i++) {
  if (copy_to_user(&buf[i], &val, 1)) {
   retval = -EFAULT;
   goto out;
  }
 }
 retval = count;

out:
 data->timeout_done = 0;
 
 /* restart timer */
 mod_timer(&data->timeout, jiffies + timeout_value*HZ);
 up(&data->sem);

 return retval;
}

static int test_select_close(struct inode *inode, struct file *filp)
{
 struct test_select_data *data = filp->private_data;
 printk(KERN_ALERT "Call test_select_close.\n");

 if (data) {
  del_timer_sync(&data->timeout);
  kfree(data);
 }

 return 0;
}

static int test_select_open(struct inode *inode, struct file *filp)
{
 struct test_select_data *data;
 printk(KERN_ALERT "Call test_select_open.\n");

 data = kmalloc(sizeof(struct test_select_data), GFP_KERNEL);
 if (data == NULL)
  return -ENOMEM;

 /* initialize members */
 spin_lock_init(&data->lock);
 init_waitqueue_head(&data->read_wait);
 // init_MUTEX(&data->sem); /* 新版 kernel 已不適用 */
 sema_init(&data->sem, 1);  /* 改用 sema_init */
 init_timer(&data->timeout);

 data->timeout.function = test_select_timeout;
 data->timeout.data = (unsigned long)data;

 filp->private_data = data;

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

 return 0;
}

struct file_operations fops = {
 .owner = THIS_MODULE,
 .open = test_select_open,
 .release = test_select_close,
 .read = test_select_read,
 .write = test_select_write,
 .poll = test_select_poll,
};

static int test_select_init(void)
{
 dev_t dev = MKDEV(test_select_major, 0);
 int alloc_ret = 0;
 int cdev_ret = 0;

 alloc_ret = alloc_chrdev_region(&dev, 0, num_of_dev, DRIVER_NAME);
 if (alloc_ret)
  goto error;

 test_select_major = MAJOR(dev);
 cdev_init(&test_select_cdev, &fops);
 cdev_ret = cdev_add(&test_select_cdev, dev, num_of_dev);
 if (cdev_ret)
  goto error;
 printk(KERN_ALERT "%s driver(major: %d) installed.\n", DRIVER_NAME, test_select_major);
 
 return 0;
error:
 if (cdev_ret == 0)
  cdev_del(&test_select_cdev);
 if (alloc_ret == 0)
  unregister_chrdev_region(dev, num_of_dev);
 
 return -1;
}

static void test_select_exit(void)
{
 dev_t dev = MKDEV(test_select_major, 0);

 cdev_del(&test_select_cdev);
 unregister_chrdev_region(dev, num_of_dev);
 
 printk(KERN_ALERT "%s driver removed\n", DRIVER_NAME);
}

module_init(test_select_init);
module_exit(test_select_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Wang Chen Shu");
MODULE_DESCRIPTION("This is test_select module.");

/*****************************************************************************/

Makefile 如下:

/*****************************************************************************/
KDIR="/opt/linux-source-2.6.38"
PWD=$(shell pwd)

obj-m := test_select.o

all:
 $(MAKE) -C ${KDIR} M=${PWD} modules
clean:
 $(MAKE) -C ${KDIR} M=${PWD} clean

/*****************************************************************************/

test.c 如下:
/*****************************************************************************/

#include 
#include 
#include 

#define DEVFILE "/dev/test_select0"

int main()
{
 int fd;
        fd_set rfds;
        struct timeval tv;
        int retval;
        unsigned char buf;
        ssize_t sz;
        int i;

        fd = open(DEVFILE, O_RDWR);
        if (fd == -1) {
                perror("open");
                return -1;
        }

        do {
                FD_ZERO(&rfds);
                FD_SET(fd, &rfds);
                tv.tv_sec = 5;
                tv.tv_usec = 0;

                printf("select() ...\n");
                retval = select(fd + 1, &rfds, NULL, NULL, &tv);
                if (retval == -1) {
                        perror("select");
                        break;
                }

                if (retval) {
                        break;
                }
        } while (retval == 0);   /* timeout elapsed */

        if (FD_ISSET(fd, &rfds)) {
                printf("read() ...\n");
                sz = read(fd, &buf, 1);
                printf("read() %d\n", sz);
                printf("%02x ", buf);
                printf("\n");
        }

        close(fd);

        return 0;

}

/*****************************************************************************/

執行結果如下:

# ls
Makefile test_code test_select.c
# make
make -C “/opt/linux-source-2.6.38″ M=/opt/test_driver/my_driver/test_select modules
make[1]: Entering directory `/opt/linux-source-2.6.38′
CC [M] /opt/test_driver/my_driver/test_select/test_select.o
/opt/test_driver/my_driver/test_select/test_select.c: In function ‘test_select_poll’:
/opt/test_driver/my_driver/test_select/test_select.c:44:1: warning: control reaches end of non-void function
Building modules, stage 2.
MODPOST 1 modules
CC /opt/test_driver/my_driver/test_select/test_select.mod.o
LD [M] /opt/test_driver/my_driver/test_select/test_select.ko
make[1]: Leaving directory `/opt/linux-source-2.6.38′
# ls
Makefile Module.symvers test_select.c test_select.mod.c test_select.o
modules.order test_code test_select.ko test_select.mod.o
# cd test_code/
# ls
test.c
# gcc test.c -o test
# ls
test test.c
# cd ..
# insmod ./test_select.ko
test_select driver(major: 250) installed.
# mknod /dev/test_select0 c 250 0
# ./test_code/test
select() …
read() …
… 經過 10秒 …
read() 1
ff
# dmesg | tail
… 以上略過 …
Call test_select_open.
Call test_select_poll.
test_select_poll returned (mask 0x104)
Call test_select_timeout.
Call test_select_close.

# rm -rf test_code/test
# rm /dev/test_select0
# rmmod test_select
# make clean
make -C “/opt/linux-source-2.6.38″ M=/opt/test_driver/my_driver/test_select clean
make[1]: Entering directory `/opt/linux-source-2.6.38′
CLEAN /opt/test_driver/my_driver/test_select/.tmp_versions
CLEAN /opt/test_driver/my_driver/test_select/Module.symvers
make[1]: Leaving directory `/opt/linux-source-2.6.38′

OK, 一切就是如此的順利,世界就是如此的美好 ^^

註記及聲明:
本教學,是參考Linux Device Driver Programming驅動程式設計由平田豐著的這本書。

發表迴響