一.globalmem设备驱动示例

字符设备是3大类设备(字符设备、块设备和网络设备)中的一类,其驱动程序完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operations结构体中的操作函数,实现 file_operations结构体中的read()、write()和ioctl()等函数是驱动设计的主体工作。       

大多数Linux驱动遵循一个“潜规则” ,那就是将文件的私有数据private_data指向设 备结构体,再用read()、write()、ioctl()、llseek()等函数通过private_data访问设备结构体。私有 数据的概念在Linux驱动的各个子系统中广泛存在,实际上体现了Linux的面向对象的设计思想。

使用文件私有数据的globalmem的设备驱动,*ppos是要读的位置相对于文件开头的偏移

#include <linux/module.h>
#include <linux/fs.h>
#include <linux/init.h>
#include <linux/cdev.h>
#include <linux/slab.h>
#include <linux/uaccess.h>
 
#define GLOBALMEM_SIZE 0x1000
#define MEM_CLEAR 0x1
#define GLOBALMEM_MAJOR 230
 
static int globalmem_major = GLOBALMEM_MAJOR;
module_param(globalmem_major, int, S_IRUGO);
 
struct globalmem_dev {
    struct cdev cdev;
    unsigned char mem[GLOBALMEM_SIZE];
};
 
struct globalmem_dev *globalmem_devp;
 
static int globalmem_open(struct inode *inode, struct file *filp)
{
    filp->private_data = globalmem_devp;
    return 0;
}
 
static int globalmem_release(struct inode *inode, struct file *filp) 37{
    return 0;
}
 
static long globalmem_ioctl(struct file *filp, unsigned int cmd, 42 unsigned long arg)
 {
  struct globalmem_dev *dev = filp->private_data;
 
  switch (cmd) {
      case MEM_CLEAR:
      memset(dev->mem, 0, GLOBALMEM_SIZE);
      printk(KERN_INFO "globalmem is set to zero\n");
      break;

     default:
     return -EINVAL;
  }
 
  return 0;
}
 
 static ssize_t globalmem_read(struct file *filp, char __user * buf, size_t size, 60  loff_t * ppos)
{
 unsigned long p = *ppos;
 unsigned int count = size;
  int ret = 0;
  struct globalmem_dev *dev = filp->private_data;
 
  if (p >= GLOBALMEM_SIZE)
  return 0;
  if (count > GLOBALMEM_SIZE - p)
  count = GLOBALMEM_SIZE - p;
 
  if (copy_to_user(buf, dev->mem + p, count)) {
  ret = -EFAULT;
  }else{
     *ppos += count;
     ret = count;
 
  printk(KERN_INFO "read %u bytes(s) from %lu\n", count, p);
  }
 
  return ret;

}
 
static ssize_t globalmem_write(struct file *filp, const char __user * buf,
  size_t size, loff_t * ppos)
{
  unsigned long p = *ppos;
  unsigned int count = size;
  int ret = 0;
  struct globalmem_dev *dev = filp->private_data;
 
  if (p >= GLOBALMEM_SIZE)
  return 0;
  if (count > GLOBALMEM_SIZE - p)
  count = GLOBALMEM_SIZE - p;
 
  if (copy_from_user(dev->mem + p, buf, count))
     ret = -EFAULT;
  else {
    *ppos += count;
    ret = count;
    printk(KERN_INFO "written %u bytes(s) from %lu\n", count, p);
 }

 return ret;
}

static loff_t globalmem_llseek(struct file *filp, loff_t offset, int orig)
{
   loff_t ret = 0;
   switch (orig) {
      case 0:
         if (offset < 0) {
           ret = -EINVAL;
           break;
         }

   if ((unsigned int)offset > GLOBALMEM_SIZE) {
      ret = -EINVAL;
      break;
   }

   filp->f_pos = (unsigned int)offset;
   ret = filp->f_pos;
   break;

     case 1:
     if ((filp->f_pos + offset) > GLOBALMEM_SIZE) {
        ret = -EINVAL;
        break;
     }
     if ((filp->f_pos + offset) < 0) {
       ret = -EINVAL;
       break;
     }
 
    filp->f_pos += offset;
    ret = filp->f_pos;
    break;
    default:
       ret = -EINVAL;
       break;
 }
 return ret;
}

static const struct file_operations globalmem_fops = {
 .owner = THIS_MODULE,
 .llseek = globalmem_llseek,
 .read = globalmem_read,
 .write = globalmem_write,
 .unlocked_ioctl = globalmem_ioctl,
 .open = globalmem_open,
 .release = globalmem_release,
};

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
 
  int err, devno = MKDEV(globalmem_major, index);

  cdev_init(&dev->cdev, &globalmem_fops);
  dev->cdev.owner = THIS_MODULE;
  err = cdev_add(&dev->cdev, devno, 1);
  if (err)
  printk(KERN_NOTICE "Error %d adding globalmem%d", err, index);
}

static int __init globalmem_init(void)
{
   int ret;
   dev_t devno = MKDEV(globalmem_major, 0);

    if (globalmem_major)
        ret = register_chrdev_region(devno, 1, "globalmem");
    else {
       ret = alloc_chrdev_region(&devno, 0, 1, "globalmem");
       globalmem_major = MAJOR(devno);
    }

 if (ret < 0)
 return ret;

 globalmem_devp = kzalloc(sizeof(struct globalmem_dev), GFP_KERNEL);
 if (!globalmem_devp) {
        ret = -ENOMEM;
        goto fail_malloc;
 }

 globalmem_setup_cdev(globalmem_devp, 0);
 return 0;

 fail_malloc:
 unregister_chrdev_region(devno, 1);
 return ret;
}

module_init(globalmem_init);

static void __exit globalmem_exit(void)
{
    cdev_del(&globalmem_devp->cdev);
    kfree(globalmem_devp);
    unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}
module_exit(globalmem_exit);

MODULE_AUTHOR("Barry Song <baohua@kernel.org>");
MODULE_LICENSE("GPL v2")

二.阻塞和非阻塞区别

阻塞操作是指在执行设备操作时,若不能获得资源,则挂起进程直到满足可操作的条件后再进行操作。被挂起的进程进入睡眠状态,被从调度器的运行队列移走,直到等待的条件被满足。而非阻塞操作的进程在不能进行设备操作时,并不挂起,它要么放弃,要么不停地查询,直至可以进行操作为止。

        驱动程序通常需要提供这样的能力:当应用程序进行read()、write()等系统调用时,若设备的资源不能获取,而用户又希望以阻塞的方式访问设备,驱动程序应在设备驱动的xxx_read()、 xxx_write()等操作中将进程阻塞直到资源可以获取,此后,应用程序的read()、write()等调用才返 回,整个过程仍然进行了正确的设备访问,用户并没有感知到;若用户以非阻塞的方式访问设备文件,则当设备资源不可获取时,设备驱动的xxx_read()、xxx_write()等操作应立即返回,read()、 write()等系统调用也随即被返回,应用程序收到-EAGAIN返回值。

        如图所示,在阻塞访问时,不能获取资源的进程将进入休眠,它将CPU资源“礼让”给其他进程。 因为阻塞的进程会进入休眠状态,所以必须确保有一个地方能够唤醒休眠的进程,否则,进程就真的“寿 终正寝”了。唤醒进程的地方最大可能发生在中断里面,因为在硬件资源获得的同时往往伴随着一个中 断。而非阻塞的进程则不断尝试,直到可以进行I/O。

==========================================

三.等待队列

在Linux驱动程序中,可以使用等待队列(Wait Queue)来实现阻塞进程的唤醒。等待队列很早就作为 一个基本的功能单位出现在Linux内核里了,它以队列为基础数据结构,与进程调度机制紧密结合,可以 用来同步对系统资源的访问。

Linux内核提供了如下关于等待队列的操作。

1.定义“等待队列头部”
wait_queue_head_t my_queue;
2.初始化“等待队列头部”
init_waitqueue_head(&my_queue);

DECLARE_WAIT_QUEUE_HEAD()宏可以作为定义并初始化等待队列头部的“快捷方式”.
DECLARE_WAIT_QUEUE_HEAD (name)
3.定义等待队列元素
DECLARE_WAITQUEUE(name, tsk);
该宏用于定义并初始化一个名为name的等待队列元素。

4.添加/移除等待队列

void add_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);
void remove_wait_queue(wait_queue_head_t *q, wait_queue_t *wait);

add_wait_queue()用于将等待队列元素wait添加到等待队列头部q指向的双向链表中,而 remove_wait_queue()用于将等待队列元素wait从由q头部指向的链表中移除。

5.等待事件
wait_event(queue, condition)
wait_event_interruptible(queue, condition)
wait_event_timeout(queue, condition, timeout)
wait_event_interruptible_timeout(queue, condition, timeout)

等待第1个参数queue作为等待队列头部的队列被唤醒,而且第2个参数condition必须满足,否则继续 阻塞。wait_event()和wait_event_interruptible()的区别在于后者可以被信号打断,而前者不能。加上 _timeout后的宏意味着阻塞等待的超时时间,以jiffy为单位,在第3个参数的timeout到达时,不论condition 是否满足,均返回。

6.唤醒队列
void wake_up(wait_queue_head_t *queue);
void wake_up_interruptible(wait_queue_head_t *queue);

上述操作会唤醒以queue作为等待队列头部的队列中所有的进程。 wake_up()应该与wait_event()或wait_event_timeout()成对使用,而wake_up_interruptible()则 应与wait_event_interruptible()或wait_event_interruptible_timeout()成对使用。

wake_up()可唤醒处于 TASK_INTERRUPTIBLE和TASK_UNINTERRUPTIBLE的进程,而wake_up_interruptible()只能唤醒处于 TASK_INTERRUPTIBLE的进程。

7.在等待队列上睡眠
sleep_on(wait_queue_head_t *q );
interruptible_sleep_on(wait_queue_head_t *q );

sleep_on()函数的作用就是将目前进程的状态置成TASK_UNINTERRUPTIBLE,并定义一个等待队列元素,之后把它挂到等待队列头部q指向的双向链表,直到资源可获得,q队列指向链接的进程被唤醒。

interruptible_sleep_on()与sleep_on()函数类似,其作用是将目前进程的状态置成 TASK_INTERRUPTIBLE,并定义一个等待队列元素,之后把它附属到q指向的队列,直到资源可获得(q 指引的等待队列被唤醒)或者进程收到信号。

sleep_on()函数应该与wake_up()成对使用,interruptible_sleep_on()应该与 wake_up_interruptible()成对使用。

======================================

示例:一个在设备驱动中使用等待队列的模版,在进行写I/O操作的时候,判断设备是否可写,如果不可写且为阻塞I/O,则进程睡眠并挂起到等待队列。

static ssize_t xxx_write(struct file *file, const char *buffer, size_t count,
  loff_t *ppos)
 {
  ...
  DECLARE_WAITQUEUE(wait, current); /* 定义等待队列元素 */
  add_wait_queue(&xxx_wait, &wait); /* 添加元素到等待队列 */ 7
  /* 等待设备缓冲区可写 */
  do {
    avail = device_writable(...);
    if (avail < 0) {
       if (file->f_flags &O_NONBLOCK) { /* 非阻塞 */
          ret = -EAGAIN;
          goto out;
       }
   __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态 */
 schedule(); /* 调度其他进程执行 */
       if (signal_pending(current)) { /* 如果是因为信号唤醒 */19 ret = -ERESTARTSYS;
          goto out;
       }
   }
 }while (avail < 0);

 /* 写设备缓冲区 */
 device_write(...)
 out:
 remove_wait_queue(&xxx_wait, &wait); /* 将元素移出xxx_wait指引的队列 */
 set_current_state(TASK_RUNNING); /* 设置进程状态为TASK_RUNNING */
 return ret;
}

1)如果是非阻塞访问(O_NONBLOCK被设置),设备忙时,直接返回“-EAGAIN” 。

2)对于阻塞访问,会调用__set_current_state(TASK_INTERRUPTIBLE)进行进程状态切换并显示通 过“schedule()”调度其他进程执行。

3)醒来的时候要注意,由于调度出去的时候,进程状态是TASK_INTERRUPTIBLE,即浅度睡眠, 所以唤醒它的有可能是信号,因此,我们首先通过signal_pending(current)了解是不是信号唤醒的,如果 是,立即返回“-ERESTARTSYS” 。

DECLARE_WAITQUEUE、add_wait_queue这两个动作加起来完成的效果如下图所示。在 wait_queue_head_t指向的链表上,新定义的wait_queue元素被插入,而这个新插入的元素绑定了一个 task_struct(当前做xxx_write的current,这也是DECLARE_WAITQUEUE使用“current”作为参数的原因)

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐