内核线程中poll的操作

October 8th, 2015 by JasonLe's Tech 1,362 views

在用户空间我们可以使用poll()函数或者select()函数对一个设备进行轮训操作,但是在内核空间呢?虽然read()/write()在内核空间有vfs统一管理,故我们可以使用vfs_read()/vfs_write()对文件进行读取(参见)。但是我找不到vfs_poll()。要想实现poll的功能,考虑使用等待队列造个poll的轮子

如果我们设计一个字符设备,这个字符设备出现数据的时候,我们需要在适当的wake_up(),在创建内核线程中,我们需要实现一个业务逻辑:

DECLARE_WAIT_QUEUE_HEAD(my_waitqueue);
static int xxx_kernel_thread(void)
{
       DECLARE_WAITQUEUE(wait,current);
       while(1)
       {
              add_wait_queue(&my_waitqueue,&wait);
              set_current_state(TASK_INTERRUPTIBLE);

              schedule();
              set_current_state(TASK_RUNNING);
              remove_wait_queue(&my_waitqueue,&wait);
              //do_something
        }
...
}

这个唤醒操作很有意思,当我们将当前等待项加入到等待队列后,设置当前的内核线程睡眠,主动调用schedule()让出cpu,当其他的某个地方唤醒这个等待队列后,代码从schedule()下一句开始执行。然后将当前内核线程设置为运行,然后移除等待项,通过这种业务逻辑做到了内核线程的轮询。

我在编写这个代码的时候犯了一个低级错误内核线程被唤醒后,没有设置TASK_RUNNING,而直接移除等待队列,这个就会导致BUGON的产生,虽然业务逻辑可以顺利执行,有时间需要看看调度的流程,才可以透彻的理解调度的实际含义。

 

http://dashan8020.blog.163.com/blog/static/4796750420115180227132/

在内核中对文件进行读写

September 28th, 2015 by JasonLe's Tech 1,863 views

我们知道在用户态下,使用各种文件的系统调用即可对文件进行读写操作,open()、write()、read()等。这些调用最后都会通过内核的VFS模型,调用到设备驱动函数,这里我们可以简单看一下open()、write()、read()驱动函数接口:

static int xxx_open(struct inode *inode, struct file *file);
static int xxx_release(struct inode *inode, struct file *file);
static ssize_t xxx_read(struct file *filp, char __user *ubuf,
                                  size_t usize, loff_t *off);
static unsigned int xxx_poll(struct file *file, poll_table *wait);
static long xxx_ioctl(struct file *f, unsigned int cmd,
                                  unsigned long arg);
ssize_t xxx_write(struct file *filp, const char __user *ubuf,
                          size_t usize, loff_t *off)

实际上在内核驱动调用的函数传入的参数远比我们在用户态下看到的复杂的多,那么问题来了:如果在内核态下对文件进行读取?当然我们不能在内核中使用syscall了,这里我们有两种方式:1.将全部的驱动函数导出 2.使用VFS在内核态中的接口。

将全部的驱动函数导出的方式虽然可行,但是这样做等于将函数暴露在全局,不利于封装,不推荐使用这种方式。第二种是我们推荐的方式,内核为开发者提供了filp_open(),filp_close(),vfs_read(),vfs_write(),vfs_fsync()接口,我们只需要调用这些接口即可在内核态下对文件进行操作。

首先我们要包含头文件:

#include <linux/fs.h>
#include <asm/segment.h>
#include <asm/uaccess.h>
#include <linux/buffer_head.h>

当然了,很多时候我们在内核层下封装了这些接口,使其可以像用户态下open那般简单易用。不过我们要注意open的返回值不再是fd,而是struct file *类型的指针!而path就是路径,flag是读写权限。

struct file* file_open(const char* path, int flags, int rights) {
    struct file* filp = NULL;
    mm_segment_t oldfs;
    int err = 0;

    oldfs = get_fs();
    set_fs(get_ds());
    filp = filp_open(path, flags, rights);
    set_fs(oldfs);
    if(IS_ERR(filp)) {
        err = PTR_ERR(filp);
        return NULL;
    }
    return filp;
}

关闭一个文件:

void file_close(struct file* file) {
    filp_close(file, NULL);
}

读取文件的封装接口参数比较多,第一个参数是文件指针,第二个是偏移量,第三个是buffer,第四个是读取的大小。与用户态下的read()类似!

int file_read(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_read(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}   

写数据到文件中:

int file_write(struct file* file, unsigned long long offset, unsigned char* data, unsigned int size) {
    mm_segment_t oldfs;
    int ret;

    oldfs = get_fs();
    set_fs(get_ds());

    ret = vfs_write(file, data, size, &offset);

    set_fs(oldfs);
    return ret;
}

立即回写到磁盘,同步文件:

int file_sync(struct file* file) {
    vfs_fsync(file, 0);
    return 0;
}

 

http://stackoverflow.com/questions/1184274/how-to-read-write-files-within-a-linux-kernel-module
https://en.wikipedia.org/wiki/Virtual_file_system

字符驱动poll函数与select()函数的交互

September 25th, 2015 by JasonLe's Tech 1,790 views

在字符驱动中,我们经常要实现poll()的功能,具体实现在注册到file_operations 的函数中,举个例子

static unsigned int xxx_poll(struct file *file, poll_table *wait)
{
         poll_wait(file, &mp_chrdev_wait, wait);
         if (rcu_access_index(mplog.next))
                 return POLLIN | POLLRDNORM;
 
         return 0;
}

我们必须在这个函数中返回POLLIN、POLLOUT等状态,从而我们可以在用户态下使用FD_ISSET()判断数据是否到来。而其中void poll_wait(struct file *filp, wait_queue_head_t *queue, poll_table *wait);它的作用就是把当前进程添加到wait参数指定的等待列表(poll_table)中。需要注意的是这个函数是不会引起阻塞的。

这里我们实现创建了一个mp_chrdev_wait的等待队列,它会把这个轮训进程放入一个等待队列中,然后这个进程会睡眠(表现在select()上就是阻塞)。当某个条件满足时,唤醒这个等待队列,也就是唤醒了轮训进程,也就是内核通知应用程序(应用程序的select函数会感知),这个时候mask返回值中有数据。然后就会接着select操作。所以我们要在恰当的位置wake_up_interruptible(&mp_chrdev_wait)

在用户空间中的代码,我们需要使用select()轮训在这个设备上:

          int register_fd, ret;
          fd_set rds;
  
          register_fd = open(CONFIG_PATH, O_RDWR);
          if (register_fd < 0)
                 err("opening of /dev/mplog");
 
          FD_ZERO(&rds);
          FD_SET(register_fd,&rds);
  
  
          while (1) {
                  /*
                   * Proceed with the rest of the daemon.
                  */
                 memset(temp, 0, MP_LOG_LEN * sizeof(struct mp));
  
 
                  ret = select(register_fd+1,&rds,NULL,NULL,NULL);
                  if(ret < 0 )
                  {
                          close(register_fd);
                          err("select error!");
                  }
                  if(FD_ISSET(register_fd,&rds))
                         read(register_fd, temp, MP_LOG_LEN * sizeof(struct mp));
 .... 
          }

为修改的kernel生成patch

September 9th, 2015 by JasonLe's Tech 1,652 views

最近在为之前修改的内核打patch,平时对于patch只是停留在用用的地步,比如我们这里有一个palloc-3.18.21.patch,打补丁,我们只需要进入到linux-3.18.21根目录下,然后使用命令:

$patch -p1 < palloc-3.18.21.patch

如果我们想撤销,那么我们只需要下面的命令:

$patch -R -p1 < palloc-3.18.21.patch

那么patch -p0 -p1 是什么意思呢?
patch -p0 (p指的是路径,后面的数字表示去掉路径的第几部分。0,表示不去掉,为全路径)
patch -p1 (p后面的数字1,表示去掉前第一个路径)

比如我们修改了内核的某个文件,我们可以使用diff生成单个文件的patch,可以使用下面的命令

$diff -urN arch/x86/include/ ../linux-3.18.21/arch/x86/include/ > patch

比如我们我们除了修改源码,还修改了kernel的头文件,那么如何将多个补丁放在一个patch文件中呢?

方法一:

$echo patch1 >> patch2

方法二:

$diff -uNr srcDir dstDir > all.patch

http://bbs.chinaunix.net/thread-2230170-1-1.html

阻塞,非阻塞访问与异步通知的比较

August 25th, 2015 by JasonLe's Tech 1,448 views

最近在编写字符设备驱动,在使用场景上面存在不同的实现:阻塞I/O,非阻塞I/O和异步通知三种,之前都是朦朦胧胧知道三者区别,而没有认真的学习三者不同,这这篇文章中我会仔细的比较三者的区别。

设备的阻塞访问

指的是执行设备操作时如果无法回去资源,那么挂起进程,挂起的进程进入休眠状态,kernel将其从rq中移出,直到条件满足,示例代码:

char buf;
fd = open("/dev/ttyS1",O_RDWR);
...
res = read(fd,&buf,1);
if(res == 1)
   printf("%c\n",buf);

20150825112857

阻塞访问的优点就是节省CPU资源,资源没有得到满足,那么挂起即可,进程进入休眠状态,将cpu资源让给其他进程(当然如果进入休眠,那么当资源满足,我们需要一种方式唤醒这个休眠进程,可以使用信号)。阻塞I/O 一般使用等待队列来实现。

设备的非阻塞访问

指的是如果得不到资源,那么立即返回,并不挂起这个进程,我们可以不断的轮训这个设备,直到这个设备满足资源。

char buf;
fd = open("/dev/ttyS1",O_RDWR | O_NONBLOCK);
...
while(read(fd,&buf,1)!= 1)
   printf("%c\n",buf);

20150825112920

非阻塞访问的最大缺点是因为要不停的轮训设备,会浪费大量的cpu时间,但是我们可以借助sigaction通过异步通知的方式访问串口提高cpu利用率,说到非阻塞,通常会用到select() poll() 系统调用,这两个调用最后都会调用到驱动设备中的poll函数。

poll函数原型是unsigned int (* poll)(struct file *filp,struct poll_table *wait),在驱动里面,调用poll_wait() 向poll_table注册等待队列,当字符设备中存在数据时,return POLLIN,POLLRDNORM,POLLOUT。这里我们要注意:设备驱动的poll函数本身并不会阻塞,但是poll和select()系统调用会阻塞等待文件描述符集合中的至少一个可访问或者超时。

异步通知

异步通知的全程是“信号驱动的异步I/O”,也就是说一旦设备准备就绪,主动通知应用程序,这样应用程序根本就不需要查询设备状态。

20150825112911

我们可以使用信号来通知设备处理,其中STDIN_FILENO是int类型,不同于STDIN 的FILE * 类型,使用signal添加信号处理函数,使用fcntl()设置SIGIO信号被STDIN_FILENO接收,之后使用O_ASYNC 使得IO具有异步特性。

#include <sys/types.h>
#include <sys/stat.h>
#include <stdio.h>
#include <fcntl.h>
#include <signal.h>
#include <unistd.h>

#define MAX_LEN 100

void input_handler(int num)
{
        char data[MAX_LEN];
        int len;

        len = read(STDIN_FILENO,&data,MAX_LEN);
        data[len] = 0;
        printf("input:%s\n",data);
}

int main()
{
        int oflags;
        signal(SIGIO,input_handler);
        fcntl(STDIN_FILENO,F_SETOWN,getpid());
        oflags = fcntl(STDIN_FILENO,F_GETFL);
        fcntl(STDIN_FILENO,F_SETFL,oflags | O_ASYNC);

        while(1);
}

 

 

[1] UNIX 高级编程

[2] Linux 设备驱动开发

[3] http://stackoverflow.com/questions/15102992/what-is-the-difference-between-stdin-and-stdin-fileno

[4] http://www.c4learn.com/c-programming/c-reference/fread-function/