2012年1月5日 星期四

基礎 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);

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

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

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

剛試了一下,果然新舊版核心還是有差,幾個問題,稍為提出來討論一下:
1. 要用 kmalloc 及 kfree 的話,要 include <linux/slab.h>
   註:其實在上一個示範程式就已有這問題了。
   請參考: 基礎 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 <linux/init.h>
#include <linux/module.h>
#include <linux/types.h>
#include <linux/kernel.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/device.h>
#include <linux/slab.h>
#include <linux/poll.h>
#include <linux/sched.h>
#include <asm/uaccess.h>

#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 <stdio.h>
#include <fcntl.h>
#include <errno.h>

#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驅動程式設計由平田豐著的這本書。

沒有留言:

張貼留言