一、驱动实现select机制的步骤
1、首先初始化一个等待队列头
2、在驱动中实现poll函数,该函数只需做两件事情
a、使用poll_wait()函数将等待队列添加到poll_table中。
b、返回描述设备是否可读或可写的掩码。
3、在驱动的相应地方调用wake_up()函数,唤醒等待队列。
两点说明:
a、等待队列
select函数阻塞的原理,实际上是通过等待队列实现的,若对等待队列不熟悉,请看我的另一篇文章《等待队列的简单使用》。否则看以下的 “select机制内核代码走读” 会很吃力。
b、掩码值及含义
POLLIN
如果设备可被不阻塞地读, 这个位必须设置.
POLLRDNORM
这个位必须设置, 如果"正常"数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).
POLLRDBAND
这个位指示带外数据可用来从设备中读取. 当前只用在 Linux 内核的一个地方( DECnet 代码 )并且通常对设备驱动不可用.
POLLPRI
高优先级数据(带外)可不阻塞地读取. 这个位使 select 报告在文件上遇到一个异常情况, 因为 selct 报告带外数据作为一个异常情况.
POLLHUP
当读这个设备的进程见到文件尾, 驱动必须设置 POLLUP(hang-up). 一个调用 select 的进程被告知设备是可读的, 如同 selcet 功能所规定的.
POLLERR
一个错误情况已在设备上发生. 当调用 poll, 设备被报告位可读可写, 因为读写都返回一个错误码而不阻塞.
POLLOUT
这个位在返回值中设置, 如果设备可被写入而不阻塞.
POLLWRNORM
这个位和 POLLOUT 有相同的含义, 并且有时它确实是相同的数. 一个可写的设备返回( POLLOUT|POLLWRNORM).
POLLWRBAND
如同 POLLRDBAND , 这个位意思是带有零优先级的数据可写入设备. 只有 poll 的数据报实现使用这个位, 因为一个数据报看传送带外数据.
注:应当重复一下 POLLRDBAND 和 POLLWRBAND 仅仅对关联到 socket 的文件描述符有意义: 通常设备驱动不使用这些标志!
二、以按键驱动为例
驱动代码button.c
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/types.h>
#include <linux/fcntl.h>
#include <linux/mm.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/errno.h>
#include <linux/init.h>
#include <linux/device.h>
#include <linux/init.h>
#include <linux/major.h>
#include <linux/delay.h>
#include <linux/io.h>
#include <asm/uaccess.h>
#include <linux/poll.h>
#include <linux/irq.h>
#include <asm/irq.h>
#include <linux/interrupt.h>
#include <asm/uaccess.h>
#include <linux/platform_device.h>
#include <linux/cdev.h>
#include <linux/miscdevice.h>
#include <linux/sched.h>
#include <linux/gpio.h>
#include <asm/gpio.h>
#define BUTTON_NAME "poll_button"
#define BUTTON_GPIO 140
static int button_major = 0;
static int button_minor = 0;
static struct cdev button_cdev;
static struct class *p_button_class = NULL;
static struct device *p_button_device = NULL;
static struct timer_list button_timer;
static volatile int ev_press = 0;
static volatile char key_value[] = {0};
static int old_value;
static int Button_Irq = 0;
static int flag_interrupt = 1;
static DECLARE_WAIT_QUEUE_HEAD(button_waitq);
static irqreturn_t buttons_interrupt(int irq, void *dev_id)
{
if(flag_interrupt) {
flag_interrupt = 0;
old_value = gpio_get_value(BUTTON_GPIO);
mod_timer(&button_timer,jiffies + HZ/100); //启动消抖定时器,消抖时间10ms
}
return IRQ_RETVAL(IRQ_HANDLED);
}
static void button_timer_handle(unsigned long arg)
{
int tmp_value;
tmp_value = gpio_get_value(BUTTON_GPIO);
if(tmp_value == old_value) {
key_value[0] = tmp_value;
ev_press= 1; //有按键按下,唤醒等待队列
wake_up_interruptible(&button_waitq);
}
flag_interrupt = 1;
}
static int button_open(struct inode *inode,struct file *file)
{
Button_Irq = gpio_to_irq(BUTTON_GPIO);
enable_irq(Button_Irq);
if(request_irq(Button_Irq, buttons_interrupt, IRQF_TRIGGER_FALLING, "BUTTON_IRQ", NULL) != 0) {
printk("request irq failed !!! \n");
disable_irq(Button_Irq);
free_irq(Button_Irq, NULL);
return -EBUSY;
}
return 0;
}
static int button_close(struct inode *inode, struct file *file)
{
free_irq(Button_Irq, NULL);
return 0;
}
static int button_read(struct file *filp, char __user *buff, size_t count, loff_t *offp)
{
unsigned long err;
if (filp->f_flags & O_NONBLOCK) {
/*nothing to do*/
//如果没有使用select机制,并且应用程序设置了非阻塞O_NONBLOCK,那么驱动这里就不使用等待队列进行等待。
} else {
wait_event_interruptible(button_waitq, ev_press); //如果应用层没有使用select,直接读的话,这里会阻塞,直到按键按下。如果使用select机制,进来这里时ev_press为真,不会阻塞。
}
err = copy_to_user(buff, (const void *)key_value, min(sizeof(key_value), count));
key_value[0] = 0;
ev_press = 0;
return err ? -EFAULT : min(sizeof(key_value), count);
}
static unsigned int button_poll(struct file *file, struct poll_table_struct *wait)
{
unsigned int mask = 0;
//将等待队列添加到poll_table中
poll_wait(file, &button_waitq, wait);
if(ev_press) {
//返回描述设备是否可读或可写的掩码
mask = POLLIN | POLLRDNORM;
}
return mask;
}
static const struct file_operations button_fops = {
.owner = THIS_MODULE,
.open = button_open,
.release = button_close,
.read = button_read,
.poll = button_poll,
//.write = button_write,
//.ioctl = button_ioctl
};
static int button_setup_cdev(struct cdev *cdev, dev_t devno)
{
int ret = 0;
cdev_init(cdev, &button_fops);
cdev->owner = THIS_MODULE;
ret = cdev_add(cdev, devno, 1);
return ret;
}
static int __init button_init(void)
{
int ret;
dev_t devno;
printk("button driver init...\n");
init_timer(&button_timer);
button_timer.function = &button_timer_handle;
if(button_major) {
devno = MKDEV(button_major, button_minor);
ret = register_chrdev_region(devno, 1, BUTTON_NAME);
} else {
ret = alloc_chrdev_region(&devno, button_minor, 1, BUTTON_NAME);
button_major = MAJOR(devno);
}
if(ret < 0) {
printk("get button major failed\n");
return ret;
}
ret = button_setup_cdev(&button_cdev, devno);
if(ret) {
printk("button setup cdev failed, ret = %d\n",ret);
goto cdev_add_fail;
}
p_button_class = class_create(THIS_MODULE, BUTTON_NAME);
ret = IS_ERR(p_button_class);
if(ret) {
printk(KERN_WARNING "button class create failed\n");
goto class_create_fail;
}
p_button_device = device_create(p_button_class, NULL, devno, NULL, BUTTON_NAME);
ret = IS_ERR(p_button_device);
if (ret) {
printk(KERN_WARNING "button device create failed, error code %ld", PTR_ERR(p_button_device));
goto device_create_fail;
}
return 0;
device_create_fail:
class_destroy(p_button_class);
class_create_fail:
cdev_del(&button_cdev);
cdev_add_fail:
unregister_chrdev_region(devno, 1);
return ret;
}
static void __exit button_exit(void)
{
dev_t devno;
printk("button driver exit...\n");
del_timer_sync(&button_timer);
devno = MKDEV(button_major, button_minor);
device_destroy(p_button_class, devno);
class_destroy(p_button_class);
cdev_del(&button_cdev);
unregister_chrdev_region(devno, 1);
}
module_init(button_init);
module_exit(button_exit);
MODULE_AUTHOR("Jimmy");
MODULE_DESCRIPTION("button Driver");
MODULE_LICENSE("GPL");
驱动Makefile文件
ifneq ($(KERNELRELEASE),)
obj-m := button.o
else
KERNELDIR ?= /ljm/git_imx6/linux-fsl/src/linux-3-14-28-r0
TARGET_CROSS = arm-none-linux-gnueabi-
PWD := $(shell pwd)
default:
$(MAKE) ARCH=arm CROSS_COMPILE=$(TARGET_CROSS) -C $(KERNELDIR) M=$(PWD) modules
endif
install:
$(MAKE) ARCH=arm CROSS_COMPILE=$(TARGET_CROSS) -C $(KERNELDIR) M=$(PWD) modules_install
clean:
rm -rf *.o *~ core .depend .*.cmd *.ko *.mod.c .tmp_versions *.symvers *.order
应用程序main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <linux/ioctl.h>
#define DEV_BUTTON "/dev/poll_button"
int main(void)
{
int dev_fd;
int ret;
char read_buf[20] = {-1};
struct timeval rto;
fd_set read_fds;
rto.tv_sec = 10;
rto.tv_usec = 0;
dev_fd = open(DEV_BUTTON, O_RDWR /*| O_NONBLOCK*/);
if ( dev_fd == -1 ) {
printf("open %s failed, ret = %d\n", DEV_BUTTON, dev_fd);
return -1;
}
while(1)
{
rto.tv_sec =10;
rto.tv_usec = 0;
FD_ZERO(&read_fds);
FD_SET(dev_fd, &read_fds);
ret = select(dev_fd+1, &read_fds, NULL, NULL, &rto);
if(ret == -1) {
printf("error\n");
continue;
} else if(ret == 0) {
printf("timeout\n");
continue;
} else {
if(FD_ISSET(dev_fd, &read_fds)) {
read(dev_fd, read_buf, 1);
printf("button pressed, val = %d\n", read_buf[0]);
}
}
}
printf("clsoe %s\n", DEV_BUTTON);
close(dev_fd);
return 0;
}
应用程序Makefile
WORKDIR =
INCLUDES = -I.
LIBS =
LINKS = -lpthread
CC = arm-none-linux-gnueabi-gcc
TARGET = main
src=$(wildcard *.c ./callback/*.c)
C_OBJS=$(patsubst %.c, %.o,$(src))
#C_OBJS=$(dir:%.c=%.o)
compile:$(TARGET)
$(C_OBJS):%.o:%.c
$(CC) $(CFLAGS) $(INCLUDES) -o $*.o -c $*.c
$(TARGET):$(C_OBJS)
$(CC) -o $(TARGET) $^ $(LIBS) $(LINKS)
@echo
@echo Project has been successfully compiled.
@echo
install: $(TARGET)
cp $(TARGET) $(INSTALL_PATH)
uninstall:
rm -f $(INSTALL_PATH)/$(TARGET)
rebuild: clean compile
clean:
rm -rf *.o $(TARGET) *.log *~
三、select的整体流程
应用层的select函数会调用到内核函数do_select,do_select调用驱动的poll函数,若poll函数返回的掩码不可读写,那么do_select进入睡眠阻塞。要从睡眠中醒来并且跳出,有两种情况:a、超时跳出;b、驱动中唤醒等待队列,这时do_select再次调用poll函数,如果poll函数返回的掩码可读写,那么就跳出阻塞,否则继续睡眠。注意:上述是在select函数设成阻塞的情况,select函数可以设置成非阻塞的(将select函数的timeout参数设置成0)。
四、select机制内核代码走读
调用顺序如下select() -> core_sys_select() -> do_select() -> fop->poll()
1、select函数解析
<pre name="code" class="objc">SYSCALL_DEFINE5(select, int, n, fd_set __user *, inp, fd_set __user *, outp,
fd_set __user *, exp, struct timeval __user *, tvp)
{
struct timespec end_time, *to = NULL;
struct timeval tv;
int ret;
if (tvp) {// 如果超时值非NULL
if (copy_from_user(&tv, tvp, sizeof(tv))) // 从用户空间取数据到内核空间
return -EFAULT;
to = &end_time;
// 得到timespec格式的未来超时时间
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
return -EINVAL;
}
ret = core_sys_select(n, inp, outp, exp, to); // 关键函数
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);
/*如果有超时值, 并拷贝离超时时刻还剩的时间到用户空间的timeval中*/
return ret; // 返回就绪的文件描述符的个数
}
2、core_sys_select函数解析
int core_sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timespec *end_time)
{
fd_set_bits fds;
/**
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)];
// 256/32 = 8, stack中分配的空间
/**
@ include/linux/poll.h
#define FRONTEND_STACK_ALLOC 256
#define SELECT_STACK_ALLOC FRONTEND_STACK_ALLOC
**/
ret = -EINVAL;
if (n < 0)
goto out_nofds;
/* max_fds can increase, so grab it once to avoid race */
rcu_read_lock();
fdt = files_fdtable(current->files); // RCU ref, 获取当前进程的文件描述符表
max_fds = fdt->max_fds;
rcu_read_unlock();
if (n > max_fds) // 如果传入的n大于当前进程最大的文件描述符,给予修正
n = max_fds;
/*
* We need 6 bitmaps (in/out/ex for both incoming and outgoing),
* since we used fdset we need to allocate memory in units of
* long-words.
*/
size = FDS_BYTES(n);
// 以一个文件描述符占一bit来计算,传递进来的这些fd_set需要用掉多少个字
bits = stack_fds;
if (size > sizeof(stack_fds) / 6) {
// 除6,为什么?因为每个文件描述符需要6个bitmaps
/* Not enough space in on-stack array; must use kmalloc */
ret = -ENOMEM;
bits = kmalloc(6 * size, GFP_KERNEL); // stack中分配的太小,直接kmalloc
if (!bits)
goto out_nofds;
}
// 这里就可以明显看出struct fd_set_bits结构体的用处了。
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_set
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;
zero_fd_set(n, fds.res_in); // 对这些存放返回状态的字段清0
zero_fd_set(n, fds.res_out);
zero_fd_set(n, fds.res_ex);
ret = do_select(n, &fds, end_time); // 关键函数,完成主要的工作
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); // 如果有申请空间,那么释放fds对应的空间
out_nofds:
return ret; // 返回就绪的文件描述符的个数
}
3、do_select函数解析
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;
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函数实
// 现中必须要调用的poll_wait()中使用的函数。
poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1; // 如果系统调用带进来的超时时间为0,那么设置
// timed_out = 1,表示不阻塞,直接返回。
}
if (end_time && !timed_out)
slack = estimate_accuracy(end_time); // 超时时间转换
retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;
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个文件描述符对应的bitmaps
in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex; // 组合一下,有的fd可能只监测读,或者写,或者e rr,或者同时都监测
if (all_bits == 0) { // 这32个描述符没有任何状态被监测,就跳入下一个32个fd的循环中
i += __NFDBITS; //每32个文件描述符一个循环,正好一个long型数
continue;
}
// 本次32个fd的循环中有需要监测的状态存在
for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {// 初始bit = 1
int fput_needed;
if (i >= n) // i用来检测是否超出了最大待监测的fd
break;
if (!(bit & all_bits))
continue; // bit每次循环后左移一位的作用在这里,用来跳过没有状态监测的fd
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) {
wait_key_set(wait, in, out, bit);// 设置当前fd待监测的事件掩码
mask = (*f_op->poll)(file, wait);
/*
调用驱动程序中的poll函数,以evdev驱动中的
evdev_poll()为例该函数会调用函数poll_wait(file, &evdev->wait, wait),
继续调用__pollwait()回调来分配一个poll_table_entry结构体,该结构体有一个内嵌的等待队列项,
设置好wake时调用的回调函数后将其添加到驱动程序中的等待队列头中。
*/
}
fput_light(file, fput_needed);
// 释放file结构指针,实际就是减小他的一个引用计数字段f_count。
// mask是每一个fop->poll()程序返回的设备状态掩码。
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit; // fd对应的设备可读
retval++;
wait = NULL; // 后续有用,避免重复执行__pollwait()
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit; // fd对应的设备可写
retval++;
wait = NULL;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
wait = NULL;
}
}
}
// 根据poll的结果写回到输出位图里,返回给上级函数
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
/*
这里的目的纯粹是为了增加一个抢占点。
在支持抢占式调度的内核中(定义了CONFIG_PREEMPT),
cond_resched是空操作。
*/
cond_resched();
}
wait = NULL; // 后续有用,避免重复执行__pollwait()
if (retval || timed_out || signal_pending(current))
break;
if (table.error) {
retval = table.error;
break;
}
/*跳出这个大循环的条件有: 有设备就绪或有异常(retval!=0), 超时(timed_out
= 1), 或者有中止信号出现*/
/*
* If this is the first loop and we have a timeout
* given, then we convert to ktime_t and set the to
* pointer to the expiry value.
*/
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}
// 第一次循环中,当前用户进程从这里进入休眠,
// 上面传下来的超时时间只是为了用在睡眠超时这里而已
// 超时,poll_schedule_timeout()返回0;被唤醒时返回-EINTR
if (!poll_schedule_timeout(&table, TASK_INTERRUPTIBLE,
to, slack))
timed_out = 1; /* 超时后,将其设置成1,方便后面退出循环返回到上层 */
}
// 清理各个驱动程序的等待队列头,同时释放掉所有空出来的page页(poll_table_entry)
poll_freewait(&table);
return retval; // 返回就绪的文件描述符的个数
}
---------------------
作者:o倚楼听风雨o
来源:CSDN
原文:https://blog.csdn.net/silent123go/article/details/52577648
版权声明:本文为博主原创文章,转载请附上博文链接!