字符设备驱动程序编写基础

《字符设备驱动程序编写基础》

本文档的Copyleft归rosetta所有,使用GPL发布,可以自由拷贝、转载,转载时请保持文档的完整性。

参考资料:《Linux设备驱动程序 第三版》,scull源码,Linux内核源码
来源:http://blog.csdn.net/rosetta/article/details/7563606

  本文写了字符设备驱动编写的基础知识,以自己学习过程来记录,写的内容比较具体,也可以说是《Linux设备驱动程序》第三章字符设备驱动程序的读书笔记。
主要写怎么生成字符设备文件?字符设备文件怎么和内核模块挂钩?怎么编写一个简单的字符设备模块插入内核?怎么访问字符设备模块注册的函数?
适合入门级新手。
使用代码示例源于scull源码。

 1、基本概念
    *对字符设备的访问是通过文件系统的设备名称进行的,文件系统内的设备名称可通过ls /dev -l命令查看,第一列用"c"标识的为字符设备文件,
    即访问字符设备是通过访问字符设备驱动的设备文件达到的。
    *主设备号和次设备号
  ls /dev -al
  crw-r-----  1 root kmem     1,   1 04-29 11:57 mem
  crw-rw----  1 root audio   14,   2 04-29 11:58 midi
  crw-rw----  1 root audio   14,   0 04-29 11:58 mixer
  crw-r--r--  1 root root   252,   0 04-29 12:19 my_module
  crw-r--r--  1 root root   260,   1 04-29 12:20 my_module1
  其中的1,14,252,260为主设备号,1, 2, 0为次设备号。
  主设备号用来标识设备对应的驱动程序。
  而这些设备文件可由自行创建,以上的my_module使用252号驱动程序及my_module1使用260号驱动都是由我自己添加的(当然现我在这个驱动是假使的,是不存在的)
  添加字符设备文件命令:mknod  /dev/my_module1  c 260 1(添加后没有任何作用,需要注册后才和驱动挂钩起作用,一会会有实例)

  当前系统有哪些驱动程序可通过
  [root@xxx scull]# cat /proc/devices
  Character devices:
    1 mem
    4 /dev/vc/0
    4 tty
    4 ttyS
    5 /dev/tty
    5 /dev/console
    5 /dev/ptmx
    6 lp
    7 vcs
   10 misc
   13 input
   14 sound
   21 sg
   29 fb
  116 alsa
  128 ptm
  136 pts
  162 raw
  180 usb
  189 usb_device
  253 scull  //register_chrdev_region注册的。
  253 scullp
  253 sculla

  次设备号由内核使用,用于正确确定设备文件所指的设备,因为多个设备文件可使用同一驱动,但需要通过次设备号来区别,这样通过次设备号可以获得一个指向内核设备的指针,
  也可以理解为数组的索引,指针的偏移量。(次设备号具体有什么作用,没接触过这方面的代码,所以没什么体会)

2,在建立字符设备前,驱动程序需要首先获得设备号,可以指定一个未使用的设备号,也可以动态分配。
   当驱动程序获得了一个设备号后,怎么和设备文件联系起来呢?比如当前驱动程序获得的设备号为252
   那么可以使用类似mknod /dev/my_module c 252 0 来建立一个设备文件,驱动程序把此主设备号的设备文件向内核注册后即可使用。
 
  下面看实例:
  首先创建设备文件:
  mknod /dev/scull0 c 253 0 (scull源码目录下的scull/scull_load可自动生成)

  分配主从设备号
    int result, i;
    dev_t dev = 0;
  if (scull_major) {//主动指定主设备号
        dev = MKDEV(scull_major, scull_minor);//把主从设备号转换成dev_t类型
        result = register_chrdev_region(dev, scull_nr_devs, "scull");//最后一个参数“scull"就是cat /proc/devices显示的,如上面显示。
    } else {//如果不指定,则由内核自动分配主从设备号
        result = alloc_chrdev_region(&dev, scull_minor, scull_nr_devs,
                "scull");
        scull_major = MAJOR(dev);
    }
    if (result < 0) {
        printk(KERN_WARNING "scull: can't get major %d\n", scull_major);
        return result;
    }

   分配了主从设备号后,怎么和刚才建立的设备文件/dev/scull0关联起来呢?那就需要向内核注册了
   注册的时候需要一个关键的结构体struct cdev,最关键的一个成员ops,cdev结构可以封闭在自已定义
  的结构体中,也可以直接就申请一个此结构体。看scull源码:
  struct scull_dev *scull_devices  //他的这个scull_dev就是自己定义的,里面包含有cdev结构体
  scull_setup_cdev(&scull_devices[i], i)
    static void scull_setup_cdev(struct scull_dev *dev, int index)
  {
      int err, devno = MKDEV(scull_major, scull_minor + index);
 
      cdev_init(&dev->cdev, &scull_fops);
      dev->cdev.owner = THIS_MODULE;//默认
      dev->cdev.ops = &scull_fops;//cdev结构体的ops在此初始化。
      err = cdev_add (&dev->cdev, devno, 1);//真正告诉内核我初始化化完成,即向内核注册。devno
      //对就的函数cdev_del从系统中移除一个设备。
      /* Fail gracefully if need be */
      if (err)
          printk(KERN_NOTICE "Error %d adding scull%d", err, index);
  }
    
  struct file_operations scull_fops = {//这个结构体中的东西才是核心啊!
      .owner =    THIS_MODULE,
      .llseek =   scull_llseek,
      .read =     scull_read,
      .write =    scull_write,
      .ioctl =    scull_ioctl,
      .open =     scull_open,
      .release =  scull_release,
  };

  因为单位历史因素,从一开始的2.4内核搞到现的2.6内核,所以还在延续使用老一设备接口,他们是:
  register_chrdev()
  和unregister_chrdev()

3,到目前为止一个简单的设备驱动模块就已经完成(完整源码请参考scull)。那么我们怎么对其操作和使用呢?
  可以直接到scull/scull/ make 再执行./scull_load,进行简单的测试
 
  读,通过cat /dev/scull进行
  写,可以通过ls -al > /dev/scull

  下面再写一个测试程序,演示用户层和内核层的读、写、ioctl操作。

  //其中的test.h是由scull.h改过来的。
  #include <stdio.h>
  #include <string.h>
  #include <stdlib.h>
 
  #include <sys/types.h>
  #include <sys/stat.h>
  #include <fcntl.h>
  #include <sys/ioctl.h>
  #include <linux/sysctl.h>
 
  #define TESTFILE "/dev/scull"
  #include "test.h"
 
  int main(int argc, char *argv[])
  {
      int fd = -1;
      fd = open(TESTFILE, O_RDWR);
      if(fd < 0)
      {
          printf("open error.\n");
          return -1;
      }
 
      char buf[30];
      if(strncmp(argv[1], "write", 5) == 0)//读和写都是比较简单的,也是容易理解。ioctl费了点神。
      {
          printf("write.\n");
          write(fd, "hello scull.\n", 20);
      }else if(strncmp(argv[1], "read", 4) == 0)
      {
          printf("read.\n");
          read(fd, buf, 20);
          printf("read buf:%s\n", buf);
      }else if(strncmp(argv[1], "ioctl", 4) == 0)
      {
          printf("ioctl.\n");
          int tmp = 111;//题外话,现在喜欢用这种风格,如果把局部变量都一起定义在开头固然整洁,但不方便看代码,还是什么时候用什么定义好。
          int ret = -1;
          if(ret = ioctl(fd, SCULL_IOCQQUANTUM, &tmp) < 0)//执行这个就是掉了内核里的scull_ioctl(),SCULL_IOCQQUANTUM
          {
              printf("ioctl error\n");
              return -1;
          }
          printf("ret:%d, tmp:x0%0x, %d\n", ret, &tmp, tmp);//可以在ko模块中把对应的值打出来对比
      }
 
      close(fd);
 
      return 0;
  }

  [root@xxx scull]# gcc test.c -Wall -g -o app


  //内核层ioctl,用户层的ioctl传进来时,cmd = SCULL_IOCQQUANTUM, arg = tmp(地址相同);
  int scull_ioctl(struct inode *inode, struct file *filp,
                 unsigned int cmd, unsigned long arg)
 
    //截取部分内核层ioctl
     switch(cmd) {

      case SCULL_IOCRESET:
        scull_quantum = SCULL_QUANTUM;
        scull_qset = SCULL_QSET;
        break;

      case SCULL_IOCSQUANTUM: /* Set: arg points to the value */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        retval = __get_user(scull_quantum, (int __user *)arg);
        break;

      case SCULL_IOCTQUANTUM: /* Tell: arg is the value */
        if (! capable (CAP_SYS_ADMIN))
            return -EPERM;
        scull_quantum = arg;
        break;

      case SCULL_IOCGQUANTUM: /* Get: arg is pointer to result */
        retval = __put_user(scull_quantum, (int __user *)arg);
        break;

      case SCULL_IOCQQUANTUM: /* Query: return it (it's positive) */
        {//自己改了下这个case分析。
        printk("SCULL_IOCQQUANTUM\n");//打印了此字符串。
        retval = __put_user(200, (int __user *)arg);//再把arg设为200.
        //return scull_quantum;
        return retval;
        }

  下面再帖一段调试结果:
  [root@xxx scull]# ./app write
  write.
  [root@xxx scull]# ./app read
  read.
  read buf:hello scull.
 
  [root@xxx scull]# ./app ioctl
  ioctl.
  ret:0, tmp:x0bfd01af0, 200
  [root@xxx scull]# dmesg
  in func:scull_open
  buf:hello scull.
 
  in func:scull_release
  in func:scull_open
  kernel read buf:hello scull.
 
  in func:scull_release
  in func:scull_open
  in func:scull_ioctl
  cmd :27399
  arg:0xbfd01af0
  _IOC_TYPE(cmd):107
  SCULL_IOC_MAGIC:107
  SCULL_IOCQQUANTUM
  in func:scull_release
  [root@panlimin scull]#

                         
  本人暂时也只能理解到这么多,至于ioctl能干什么?可以从内核获得一些数据这是必然的,
看书上说还可以对其进行一系列的控制,这当然肯定可以,这么多case分支,用户层选择适合自己
的cmd就可以完成对应的操作。除了这些,它还能干吗?或者说ioctl的用法? 除了read、write、
ioctl外,其它的比如mmap、poll操作实现等,内存共享、异步等原理的学习理解等都需要进一步
去研究学习。


  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值