2014/10/16

[轉]基礎 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.oall: $(MAKE) -C ${KDIR} M=${PWD} modulesclean: $(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驅動程式設計由平田豐著的這本書。

沒有留言:

張貼留言