简单驱动word_count

目的:写一个统计单词个数的简单驱动。

1、驱动的概念

1.1、什么是驱动

无操作系统时函数可以由工程师自己定义。
有操作系统时,把单一的“驱使硬件设备行动”变成了操作系统内与硬件交互的模块,它对外呈现为操作系统的 API。
对设备驱动最通俗的解释就是“驱使硬件设备行动”。设备驱动充当了硬件和应用软件之间的纽带,应用软件时只需要调用系统软件的应用编程接口( API)就可让硬件去完成要求的工作。

1.2 为什么要有驱动

任何一个计算机系统的运转都是系统中软硬件共同努力的结果,但是软硬件工程师不想涉及对方的领域,因此这个中间的连接需要驱动工程师来解决。

1.3 驱动所在位置

linux设备驱动是以内核模块的形式出现。模块的概念,可以不编入内核镜像,在使用的时候动态的加入到内核中,模块被加载后和其他内核完全一样。


image.png
image.png

2、linux字符设备驱动结构

2.1 重要的两个结构体

2.1.1 cdev结构体

linux内核中,使用cdev(character dev)结构体描述一个字符设备,结构体如下:

image.png

  1. dev_t:定义了设备号,为32位,12位为主设备号,20位为次设备号。
    使用MAJOR(dev_t dev)、MINOR(dev_t dev)可以获得,使用MKDEV(int major, int minor)可以通过主设备号和次设备号生成dev_t:

2.1.2 file_operations 结构体

file_operations:定义了字符设备驱动提供给VFS的接口函数,是 字符设备驱动程序设计的主题内容,会在应用程序进行linus的open/read()等系统调用时最终被内核调用

struct file_operations {
        **struct module *owner;**
//指向module,一般初始化为THIS_MODULE
        loff_t (*llseek) (struct file *, loff_t, int);
//修改一个文件的当前读写位置,并将新位置返回;指针参数filp为进行读取信息的目标文件结构体指针,
//loff_t用来维护当前读写位置,代表用户空间的文件光标移动数量值
//int 代表移动光标的参考位置,有3种(即当前光标、文件开头、文件结尾)
//移动设备的文件指针,然后读/写接口就可以对移动后的位置进行读取功能,而不是每次读/写都只能从0开始一次读取全部数据。llseek函数改变了filp->f_pos(position),返回新的位置
//实现的功能是修改驱动中write/read函数的文件指针。
        **ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);**
// 从设备中读取数据,成功时返回读取字节数
//PS:这里是_ _user
       **ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);**
        ssize_t (*aio_read) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
//对文件描述符对应的设备进行异步读操作。
        ssize_t (*aio_write) (struct kiocb *, const struct iovec *, unsigned long, loff_t);
        int (*readdir) (struct file *, void *, filldir_t);
//对于设备文件这个成员应当为 NULL; 它用来读取目录, 并且仅对文件系统有用.
        unsigned int (*poll) (struct file *, struct poll_table_struct *);
//返回设备资源的可获取状态,询问是否可被非阻塞地立即读写。
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
// 提供设备相关控制命令的实现。
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
// 
        int (*mmap) (struct file *, struct vm_area_struct *);
// 将设备内存映射到进程的虚拟地址空间,对帧缓冲等设备特别有意义,
//帧缓冲被映射到用户空间后,应用程序可以直接访问无需在内核和应用间进行内存复制。
//帧缓冲是Linux为显示设备提供的一个接口,帧缓存的每一存储单元对应屏幕上的一个像素,整个帧缓存对应一帧图像。
      ** int (*open) (struct inode *, struct file *);**
//打开设备驱动
//inode 为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
//filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*aio_fsync) (struct kiocb *, int datasync);
        int (*fasync) (int, struct file *, int);
        int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,loff_t len);
        int (*show_fdinfo)(struct seq_file *m, struct file *f);
};

2.2 linux字符设备驱动的组成

  1. 字符设备驱动模块加载与卸载函数
    加载函数:设备号的申请和cdev的注册
    卸载函数:设备号的释放和cdev的注销
    image.png

memset是计算机中C/C++语言初始化函数。作用是将某一块内存中的内容全部设置为指定的值, 这个函数通常为新申请的内存做初始化工作。extern void *memset(void *buffer, int c, int count) buffer:为指针或是数组, c:是赋给buffer的值, count:是buffer的长度。
cdev_add 和cdev_del分别向系统添加和删除一个cdev,完成字符设备的注册和注销。

struct xxx_dev_t{
  struct cdev cdev;
  ...
} xxx_dev;
static int _ _init xxx_init(void)
{
  cdev_init(&xxx_dev.cdev, &xxx_fops);
  xxx_dev.cdev.owner = THIS_MODULE;
  ret = cdev_add(&xxx_dev.cdev,xxx_dev_no,1);

static void _ _exit xxx_exit(void)
{
  unregister_chrdev_region{xxx_dev_no,1);
//释放设备号
  cdev_del{&xxx_dev.cdev};
}
  1. 字符设备驱动的file_operations结构体的成员函数
    file_operations中的成员函数是字符设备驱动与内核虚拟文件系统的接口,是用户空间对linux进行系统调用的最终落实者。以read()为例
ssize_t xxx_read(struct file *filp, char _ _ user *buf, size_t count, loff_t *f_pos)
{
  ...
  copy_to_user(buff,...,...);
}

这里:完成内核空间和用户空间内存复制的是
copy_from_user() 、copy_to_user()
完全复制成功,返回值为0.

image.png

3、编写一个驱动

3.1 编写linux驱动程序的步骤

  1. 宏定义与头文件
  2. 注册和注销设备文件
  3. 指定与驱动相关的信息
  4. 指定回调函数
  5. 编写业务逻辑
  6. 编写Makefile文件

经过上面的6个步骤,可以安装和卸载linux驱动

3.2 编写word_count驱动

3.2.1 宏定义

#define DEVICE_NAME "wordcount"
#define DEVICE_COUNT 1
#define word_count_MAJOR 0
#define word_count_MINOR 234
#define TRUE -1
#define FALSE 0
static unsigned char mem[10000]
static int major = word_count_MAJOR;
static int minor = word_count_MAJOR;
static dev_t dev_number;

3.2.3 注册和注销设备文件

//
// 定义file_operations结构体
 static struct file_operations dev_fops =
{.owner = THIS_MODULE, .read = word_count_read, .write = word_count_write};

// 定义cdev结构体,用于描述字符设备
static struct cdev word_count_cdev;
//
//
// 创建设备文件
static int word_count_create_device(void)
{
  int ret = 0;
  int err = 0;

//初始化cdev的成员,并建立cdev和file_operations之间的连接
  cdev_init(&word_count_cdev,&dev_fops);
  wordcount_cdev.owner = THIS_MODULE;
if (major >0)
{
// MKDEV:可通过主设备号和次设备号生成dev_t
  dev_number = MKDEV(major,minor);
// #define DEVICE_NAME "wordcount"
// #define DEVICE_COUNT 1
// 注册字符设备区
  err = register_chrdev_region(dev_number,DEVICE_COUNT,DEVICE_NAME);
  if (err < 0) 
  {   printk(kern_warning " register_chrdev_region() failed\n");
    retun err;
  }
else
  {
//随机分配设备号,并注册字符设备区
    err = alloc_chrdev_region(&word_count_cdev.dev,10,DEVICE_COUNT,DEVICE_NAME);
  if (err < 0) 
  {   printk(kern_warning " register_chrdev_region() failed\n");
      retun err;
  }
  major = MAJOR(word_count_cdev.dev);
  minor = MINOR(word_count_cdev.dev);
  dev_number = word_count_cdev.dev;
}
//添加字符设备
ret = cdev_add(&word_count_cdev,dev_number,DEVICE_COUNT);
word_count_class = class_create(TIHIS_MODULE,DEVICE_NAME);
//创建设备文件
device_create(word_count_class,NULLndev_number,DEVICE_NAME);
return ret;
}

// 销毁字符设备
static void word_count_destory_decice(void)
{
 // 销毁字符设备
device_destroy(word_count_class,dev_number);
// 销毁class 结构体
if (word_count_class)
  class_destroy(word_count_class);
// 注销字符设备区
unregister_chrdev_region(dev_number, DEVICE_COUNT);
return;
}


// 初始化linux wordcount驱动
static int word_count_init(void)
{
  int ret;
  ret  = word_count_create_device();
  return ret;
}
// 卸载wordcount 驱动
static void word_count_exit(void)
{
  word_count_destroy_device();
}

//指定word_count驱动的初始化和卸载
module_init(word_count_init);
module_exit(word_count_exit);

3.2.4 指定与驱动相关的信息

 MODULE_AUTHOR("GUQUAN");
 MODULE_DESCRIPTION("statistics of word count. ");
 MODULE_ALIAS("word count module. ");
 MODULE_LICENSE("GPL ");

3.2.5 编写业务逻辑

 static char is_spacewhite(char c)
 {
      if(c ==  ' ' || c == 9 || c == 13 || c == 10)
          return TRUE;
     else
         return FALSE;
 }
//空格、回车、换行分割的字符串作为一个单词
 static int get_word_count(const char *buf)
{
     int n = 1;
     int i = 0;
     char c = ' ';
     char flag  = 0;// 处理多个空格情况:0:正常情况,1:已遇到一个空格
     if(*buf == '\0')
         return 0;
//第一个字符是空格,从0开始计数
      if(is_spacewhite(*buf) == TRUE)
          n--;
//扫描字符串中的每一个字符,假设:hello world\0
////空格、回车、换行分割的字符串作为一个单词
      for(;(c = *(buf + i)) != '\0'; i++)
      {
          if(flag == 1 && is_spacewhite(c) == FALSE)
                      flag = 0;
//多个空格时,忽略
          else if(flag == 1 && is_spacewhite(c) == TRUE)
                      continue; 

          if(is_spacewhite(c) == TRUE)
          {
              n++;
             flag = 1;
         }
      }
//如果字符串以一个或多个空格结尾,不计数
      if(is_spacewhite(*(buf + i - 1)) == TRUE)
          n--;
      return n;
  }

 //从设备文件读取数据
  static ssize_t word_count_read(struct file *file, char *buf, size_t count, loff_t *ppos)
  {
      unsigned char temp[4];
// 将单词数(int类型)分解成4个字节存储在buf中
      temp[0] =word_count >> 24;
      temp[1] =word_count >> 16;
      temp[2] =word_count >> 8;
      temp[3] =word_count;
      copy_to_user(buf,(void*) temp,4);
      printk(" read:word count:%d",(int) count);
      return count;
  }
 
  //向设备文件写入数据
static ssize_t word_count_write(struct file *file, const char *buf, size_t count, loff_t *ppos)
 {

         ssize_t written = count;
         copy_from_user(mem,buf,count);
          mem[count] = '\0';
          
         word_count = get_word_count(mem);
          
         printk("writen:word count :%d",(int)word_count);
         return written;
 }

3.2.6 编写Makefile文件

obj-m += word_count.o

3.2.7 安装和卸载word_count驱动

安装:insmod word_count.ko
卸载:rmmod word_count

3.2.8 test_main

  7 int main(int argc,char *argv[])
  8 {
  9      int testdev;
 10     unsigned char buf[4];
 11     testdev =  open("/dev/wordcount",O_RDWR);
 12
 13     if(testdev == -1)
 14     {
 15         printf("cann't open file,because cann't open /dev \n");
 16         return 0;
 17     }
 18     if(argc >1)
 19     {
 20         write(testdev,argv[1],strlen(argv[1]));
 21         printf("string:%s\n", argv[1]);
 22     }
 23
 24     read(testdev,buf,4);
 25
 26     int n = 0;
 27     n = ((int) buf[0]) << 24 | ((int) buf[1]) << 16 | ((int) buf[2]) << 8 | ((int) buf[3]);
 28     printf("word byte display :%d,%d,%d,%d\n", buf[0],buf[1],buf[2],buf[3]);
 29
 30     printf("word count : %d\n",n);
 31     close(testdev);
 32     return 0;
 33 }

4、遇到的问题

  1. 挂载:insmod
    提示:operation not permitted
    解决使用sudo insmod word_count.ko
  2. ./test_word_count时,提示cann't open file,然后使用命令查看权限:ll /dev
    可以看到如下信息:
    crw --- --- 1 root root 10, 58 8月 13 16:33 wordcount
    image.png

文件类型
linux一共由7种文件类型,分别如下:
-:普通文件
d:目录文件
l:软链接(类型Windows的快捷方式)
b:块设备文件(硬盘、光驱)
p:管道文件
c:字符设备文件
s:套接口文件/数据接口文件(例如启动Mysql服务器时产生的mysql.sock文件)

所有者(u表示)权限,组群(group),其他人(other)
文件权限:
| r(read) | 4 | 可读 |
| w(write) | 2 | 可写 |
| x(execute) | 1 | 可执行 |

sudo chmod 777 /dev/wordcount,然后查看,crwxrwxrwx
然后执行 ./test_word_count “hello world",可以得到如下结果


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354