《深入浅出嵌入式底层软件开发》读者请进

本博客移往腾讯QQ空间: http://user.qzone.qq.com/308337370/blog/1308149414

http://user.qzone.qq.com/308337370/blog/1293112053

http://user.qzone.qq.com/308337370/

发表在 Uncategorized | 留下评论

生日

蜀中闷热已多日,

今起大风雨飞扬。

隐形翅膀当开张,

搏击长空心翱翔!

发表在 个人感评 | 留下评论

驱动程序对ioctl的规范实现

单击,返回主页,查看更多内容

我们在LED灯的驱动中,学习了如何在驱动中实现ioctl功能函数,但这种简化的实现存在一些潜在的隐患。例如,有2个设备,一个是led灯,另一个是硬盘。这2个设备的驱动由不同的程序员编写。led灯驱动定义命令0表示亮灯,而硬盘驱动定义命令0表示格式化硬盘。这样一来就存在潜在的威胁,例如,如果应用程序员本想通过ioctl(fd,  0)点亮led灯,但在调用open的时候,误将硬盘的设备文件当成了led灯的设备文件来打开,那么当应用程序执行ioctl(fd,  0)的时候就会导致硬盘被格式化。这个问题如何解决呢?最好的解决方案就是想办法让不同设备的命令的编号落在不同的区间上,例如led的命令编号为0-10,硬盘的命令编号为20-100,以此类推,这样即使应用程序误将硬盘当作led灯来打开,但由于其执行的是ioctl(fd,  0)或者ioctl(fd,  1),命令0和1均不是硬盘对应的命令,这样只会使得ioctl执行失败,而不会导致硬盘被格式化。

正是基于这样的考虑,内核编写者给出了一个规范,使得不同设备的命令的编号处于不同的区间,所以我们在编写驱动程序时应该遵循该规范。下面就来介绍该规范。

int scull_ioctl(struct inode *inode, struct file *filp, unsigned int cmd, unsigned long arg)

ioctl的第3个参数(cmd)是命令编号,第4个参数(arg)是命令的参数。

一、规范ioctl的第3个参数

1、规范将cmd的32bit从高位到低位分为4个部分:

  • DIR:2位
    • 表示,ioctl的第4个参数如果是指针的话,该指针所指向的数据是用于读还是写(读写是站在app的角度而言)
    • #define    _IOC_NONE    0U
    • #define    _IOC_WRITE   1U
    • #define    _IOC_READ     2U
  • SIZE:14位
    • 表示,ioctl的第4个参数如果是指针的话,该指针所指向的数据的长度
  • TYPE:8位
    • 表示,统一分配的ioctl的类型编号
    • #define    SCULL_IOC_MAGIC    ‘k’
  • NR:8位
    • 表示,自己定义的命令编号(都位于同一个TYPE下)
    • #define    SCULL_IOCTQUANTUM    _IO(SCULL_IOC_MAGIC, 3)

2、系统头文件中定义的几个规范的宏

  • #define _IOC(dir,type,nr,size) (((dir)<<30) | ((type)<<8) | ((nr)<<0) |((size)<<16))
  • #define _IO(type,nr) _IOC(_IOC_NONE,(type),(nr),0)
  • #define _IOR(type,nr,datatype) _IOC(_IOC_READ,(type),(nr),(sizeof(datatype)))
  • #define _IOW(type,nr,datatype) _IOC(_IOC_WRITE,(type),(nr),(sizeof(datatype)))
  • #define _IOWR(type,nr,datatype) _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(sizeof(datatype)))

这样一来#define    SCULL_IOCTQUANTUM    _IO(SCULL_IOC_MAGIC, 3) 使得命令编号SCULL_IOCTQUANTUM为二进制的 00 00000000000000 字符k的ASCII码 00000011。由于字符k是专门分给scull驱动所使用(其它驱动不会使用k),所以只要scull驱动自己在定义自己的命令编号(NR)时保证不冲突,则这个编号在所有驱动中都不会冲突。

二、规范化的ioctl的第3、4个参数的应用(_IOC_TYPE、_IOC_NR等都是内核定义的宏,猜出它们的涵义应该不是难事吧!)

1、if (_IOC_TYPE(cmd) != SCULL_IOC_MAGIC) return -ENOTTY;

可以通过比较cmd的TYPE段与分配给本驱动的TYPE段是否相等来确定应用程序传入的命令是否是本驱动支持的命令

2、if (_IOC_NR(cmd) > SCULL_IOC_MAXNR) return -ENOTTY;

可以通过比较cmd的NR段是否大于了本驱动支持的命令的总数量来确定应用程序传入的命令是否是本驱动支持的命令(当然前提是命令的编号从1开始顺次编号,并且宏SCULL_IOC_MAXNR是命令的总数量)

3、if (_IOC_DIR(cmd) & _IOC_READ)
    err = !access_ok(VERIFY_WRITE, (void __user *)arg, _IOC_SIZE(cmd));

可以通过查看cmd的DIR段来得知ioctl是否用来从内核读、写数据到用户空间,进而调用access_ok来测试该用户缓冲区是否安全可写(或可读)

注:内核API access_ok 用于测试用户缓冲区(arg指向的内存区,大小为_IOC_SIZE(cmd ))是否安全可写(或可读)

4、__get_user利用ioctl的第4个参数

switch(cmd)
case SCULL_IOCSQUANTUM:
    if (! capable (CAP_SYS_ADMIN))
    return -EPERM;
    retval = __get_user(scull_quantum, (int __user *)arg);

注:__get_usr用于从arg所指向的用户内存区拷贝sizeof(*arg)大小的数据到scull_quantum,用于替换copy_from_user;__put_usr反之 ;capable (CAP_SYS_ADMIN)表示进程有没有管理员权限

三、scull驱动对ioctl规范化实现的代码分析

这就留给不太懒的你吧!

请查看scull驱动中的scull_ioctl函数的实现来体会ioctl的规范化实现;

请查看scull驱动中scull.h头文件以了解对ioctl各个命令的编号的规范化定义:

#define SCULL_IOCSQUANTUM _IOW(SCULL_IOC_MAGIC,  1, int)
#define SCULL_IOCSQSET    _IOW(SCULL_IOC_MAGIC,  2, int)
#define SCULL_IOCTQUANTUM _IO(SCULL_IOC_MAGIC,   3)
#define SCULL_IOCTQSET    _IO(SCULL_IOC_MAGIC,   4)
#define SCULL_IOCGQUANTUM _IOR(SCULL_IOC_MAGIC,  5, int)
#define SCULL_IOCGQSET    _IOR(SCULL_IOC_MAGIC,  6, int)
#define SCULL_IOCQQUANTUM _IO(SCULL_IOC_MAGIC,   7)
#define SCULL_IOCQQSET    _IO(SCULL_IOC_MAGIC,   8)
#define SCULL_IOCXQUANTUM _IOWR(SCULL_IOC_MAGIC, 9, int)
#define SCULL_IOCXQSET    _IOWR(SCULL_IOC_MAGIC,10, int)
#define SCULL_IOCHQUANTUM _IO(SCULL_IOC_MAGIC,  11)
#define SCULL_IOCHQSET    _IO(SCULL_IOC_MAGIC,  12)

单击,与作者交流

发表在 技术博客 | 留下评论

ARM编程进阶之四 —— 启动例程

单击,返回主页,查看更多内容

前面的讲解中我们看到了,main(或相当于main的xmain)函数并不是整个程序首先运行的部分,它会被一个称为启动例程的代码所调用。如果是用gcc编译运行在Linux操作系统上的应用程序,则启动例程是由gcc在编译时放在程序员所写代码之前的;现在我们是用ads编写裸机程序,所以需要程序员自行编写启动例程并设法将其放在整个程序的开头。

为什么需要启动例程呢?启动例程都干些什么事情?为什么启动例程总是用汇编语言写就?如何才能保证程序运行时首先运行的是启动例程?

好,先看一个

 

 

 

单击,与作者交流

发表在 技术博客 | 留下评论

块设备驱动初步

单击,返回主页,查看更多内容

本文的素材以及源代码(稍有改动)均来源于《Linux Device Driver》,因此本文可视为该书块设备驱动相关章节的阅读理解。

一、体验块设备驱动(单击下载驱动源码

本驱动模拟了一个硬盘。想想你去中关村(不过我更喜欢去成都@世界)买了一个硬盘,迫不及待地安装在你的Linux机器上,你怎么样才能使用这个新硬盘呢?当然要先把它驱动起来。

1、make生成sbull.ko后加载驱动:sudo insmod sbull.ko

2、对硬盘分区

  • 执行suod fdisk /dev/sbulla
  • 输入x,进入高级菜单
  • 输入h,change number of heads 为4
  • 输入c,change number of cylinders 为4
  • 输入r,退回主菜单
  • 顺次输入n、p、1、1、4,创建一个主分区占有cylinders 1-4
  • 输入w,保存并退出

3、输入 sudo mkfs.ext2 /dev/sbulla1 在硬盘分区上格式化ext2文件系统

4、挂载新硬盘上的分区

  • 输入 mkdir testdir创建空目录
  • 输入mount –t ext2 /dev/sbulla1 ./testdir

之后,你就可以通过testdir目录来访问新硬盘分区了。

二、块设备驱动框架介绍

1、驱动接口简介

  • 上层接口
    • 用户接口
      • 块设备 b:/dev/sda、/dev/sda2、 /dev/ram0
      • 用户空间程序
        • mkfs、mount、fdisk;
        • 直接调用open、release、ioctl接口
        • 读写时,不直接调用读写接口,而是将读写request提交给OS的block layer层
    • 操作系统接口
      • block layer I/O scheduler决定需要执行磁盘I/O时,调用驱动的读写接口
      • 抽象物理设备为 cylinder、header、sector组成,视其为编号从0开始的flat型sector集合。(注1)
      • 总假定物理设备一个sector大小为512byte(注2)
  • 下层接口
    • 抽象物理设备为编号从0开始的flat型sector集合,但转换sector大小为物理设备的实际值,通过物理寄存器的读写,将数据写入指定的sector位置
    • 由物理设备的中断(读写完成)来唤醒用户进程(或内核线程)

注1: 借助minor number辨别partition编号(也可能是整个磁盘),再借助分区表决定分区的起始sector号

注2: #define KERNEL_SECTOR_SHIFT 9             #define KERNEL_SECTOR_SIZE (1 << KERNEL_SECTOR_SHIFT)

2、块设备结构体

块设备结构体 struct gendisk 是块设备驱动中最重要的数据结构,它在操作系统和驱动中代表一个物理磁盘。其主要字段有:

  • major:磁盘对应的主设备号,出现在/proc/devices中。register_blkdev(sbull_major, "sbull");
  • first_minor:磁盘对应的第1个次设备号。dev->gd->first_minor = which*SBULL_MINORS;
  • minors :磁盘拥有的次设备号的总数。dev->gd = alloc_disk(SBULL_MINORS);
  • disk_name:磁盘的名称。出现在/proc/partitions中
  • fops:块设备驱动中的功能函数。包括:open\release\media_change\revalidate_disk\ioctl等
  • queue:OS回调读写函数时使用的request queue队列(它绑定了读写函数)
  • private_data:私有数据,常用于存放包裹设备结构体
  • capacity:512字节大小的sector的总数量
    • set_capacity(dev->gd, nsectors*(hardsect_size/KERNEL_SECTOR_SIZE));

3、块设备结构体注册

  • 申请major number
    • sbull_major = register_blkdev(sbull_major, "sbull"); //sbull出现在/proc/devices中
  • 分配(初始生成)块设备结构体
    • dev->gd = alloc_disk(SBULL_MINORS); //同时也关联设备号数量
  • 将块设备结构体与设备号、设备操作函数、读写队列关联
    • dev->gd->major = sbull_major; //关联主设备号
    • dev->gd->first_minor = which*SBULL_MINORS; //关联次设备号 
    • dev->gd->fops = &sbull_ops; //关联功能函数
    • dev->queue = blk_init_queue(sbull_request, &dev->lock);
    • dev->gd->queue = dev->queue; //关联读写队列
  • 将块设备结构体注册进OS
    • add_disk(dev->gd);

4、块设备结构体注销

  • 将块设备结构体从OS中注销
    • del_gendisk(dev->gd);
  • 销毁块设备结构体(移除对kobject的最后ref.,释放结构体内存)
    • put_disk(dev->gd);
  • 释放major number
    • unregister_blkdev(sbull_major, "sbull");

5、块设备的简单读写-原理

  • 用户程序或内核组件提出读写request时,会将该request提交给block layer层
  • block layer层构造request结构,并将其链入块设备结构体绑定的request queue中
  • 在适当的时候,block layer层回调request queue中绑定的驱动中的读写函数,并将request queue的指针作为参数传给驱动中的读写函数
  • 驱动中的读写函数根据传给它的request queue的指针,从request queue中取出一个request,根据request中指定的方向(读或写)、数据在内存中的位置、在设备上的位置(起始sector编号)、传输数据量的大小(sector数量),完成物理读写
  • 驱动通知block layer层实际完成的读写量,block layer层据此更新其内部各个数据结构;并告知驱动是否整个request已经处理完成,若是,驱动则负责将request从request queue中摘下,并释放request结构的内存,唤醒等待该request完成的所有进程

6、读写队列结构体-注册与注销

  • 分配(初始生成)读写队列结构体,并与读写函数绑定
    • dev->queue = blk_init_queue(sbull_request, &dev->lock);
      • block layer在回调读写函数sbull_request时,会先获得自旋锁dev->lock,这样block layer就可以与驱动的其它函数共享相同的临界区
    • blk_queue_hardsect_size(dev->queue, hardsect_size);
      • 设置实际设备的扇区大小,这样block layer层就会根据实际设备能够处理的扇区大小来构造request结构体,从而不会出现实际设备处理不了的request
    • dev->queue->queuedata = dev
  • 将块设备结构体与读写队列关联
    • dev->gd->queue = dev->queue;
  • 将读写队列结构体间接注册进OS
    • add_disk(dev->gd);
  • 将读写队列结构体间接从OS中注销
    • del_gendisk(dev->gd);
  • 销毁读写队列结构体(移除对kobject的最后ref.,释放结构体内存)
    • blk_cleanup_queue(dev->queue);

7、块设备的简单读写-实现

108 static void sbull_request(request_queue_t *q)
109 {
110   struct request *req;
112   while ((req = elv_next_request(q)) != NULL) {  //从request queue中取出一个request,循环直到request queue中的所有request被传送完
        //因为读写队列与块设备结构体已关联,所以block layer层在将读写请求链入request queue时,能将rq_disk字段指向块设备结构体
113     struct sbull_dev *dev = req->rq_disk->private_data;
114     if (! blk_fs_request(req)) {
116       end_request(req, 0);
117       continue;
118     }
123     sbull_transfer(dev, req->sector, req->current_nr_sectors, //根据request中指定的方向(rq_data_dir)、数据在内存中的位置( req->buffer )、
124                     req->buffer, rq_data_dir(req)); //在设备上的位置( req->sector )、传输数据量的大小( req->current_nr_sectors ),完成物理读写
125     end_request(req, 1); //通知block layer层;将request从request queue中摘下;释放request结构的内存,唤醒等待该request完成的所有进程
126   }
127 }

void end_request(struct request *req, int uptodate)
{
    //驱动通知block layer层实际完成的读写量,block layer层据此更新其内部各个数据结构;并告知驱动是否整个request已经处理完成
    if (!end_that_request_first(req, uptodate, req->hard_cur_sectors)) { 
        add_disk_randomness(req->rq_disk);   
        blkdev_dequeue_request(req);     //若是,驱动则负责将request从request queue中摘下
        end_that_request_last(req);    //释放request结构的内存,唤醒等待该request完成的所有进程
    }
}

8、简单读写的不足

  • 一次只命令硬件传输1个数据块segment(内存中不超过1page的连续单元),其只是request中的一小部分而已
  • 虽然简单读写采用while循环请求elv_next_request,但block层会认为硬件可能由于某种原因不能一次性完成一个完整request的传输,因此相邻2次elv_next_request极有可能返回的是不同的request
  • block layer尽了很大努力,实施电梯调度算法(drivers/block/ll_rw_block.c and elevator.c ),使得一个request结构体中包含多个在内存中离散,但在物理设备上却连续的数据块。
  • 先后2次elv_next_request的简单读写,使得磁头必须寻道,产生较大延迟
  • 简单读写忽视block layer层的工作,对同一个request结构体中包含的多个segment在物理设备上连续,不予理睬,实在是暴殄天物

9、请求队列

  • 一个块设备的I/O请求的序列
  • 跟踪未完成的块I/O请求
  • 允许使用多I/O调度器,以最大化性能的方式提交I/O请求给你的驱动,I/O调度器还负责合并邻近的请求
  • 请求队列的实现
    • drivers/block/Ll_rw_block.c和elevator.c

10、块设备的高效读写-原理与实现

一个request结构体中包含多个在内存中离散,但在物理设备上却连续的数据块,由bio和bio_vec结构体来表示

image image

337 static void setup_device(struct sbull_dev *dev, int which)
362         switch (request_mode) {
370             case RM_FULL:
371                 dev->queue = blk_init_queue(sbull_full_request, &dev->lock);
374                 break;
385         }
387         dev->queue->queuedata = dev;
409 }

175 static void sbull_full_request(request_queue_t *q)
176 {
177         struct request *req;
178         int sectors_xferred;
179         struct sbull_dev *dev = q->queuedata;
181         while ((req = elv_next_request(q)) != NULL) { //每次取出读写队列中的一个request
187                sectors_xferred = sbull_xfer_request(dev, req);
188                 if (! end_that_request_first(req, 1, sectors_xferred)) {
189                         blkdev_dequeue_request(req);
191                         end_that_request_last(req, 1);
192                 }
193         }
194 }

#define rq_for_each_bio(_bio, rq)    \
    if ((rq->bio))            \
        for (_bio = (rq)->bio; _bio; _bio = _bio->bi_next)

157 static int sbull_xfer_request(struct sbull_dev *dev, struct request *req)
158 {
159         struct bio *bio;
160         int nsect = 0;
162         rq_for_each_bio(bio, req) { //while循环,每次取出req中的一个bio
163                 sbull_xfer_bio(dev, bio);
164                 nsect += bio->bi_size/KERNEL_SECTOR_SIZE; //bi_size记录一个bio中数据的总字节数
165         }
167         return nsect;
168 }

#define bio_for_each_segment(bvl, bio, i)                \
    __bio_for_each_segment(bvl, bio, i, (bio)->bi_idx)

#define __bio_for_each_segment(bvl, bio, i, start_idx)            \
    for (bvl = bio_iovec_idx((bio), (start_idx)), i = (start_idx);    \
         i < (bio)->bi_vcnt;                    \
         bvl++, i++)

133 static int sbull_xfer_bio(struct sbull_dev *dev, struct bio *bio)
134 {
135         int i;
136         struct bio_vec *bvec;
137         sector_t sector = bio->bi_sector; //bi_sector记录bio中首字节应位于硬件的哪个扇区。bio中的所有segment在硬件上的sector位置全部连续
139         /* Do each segment independently. */
140         bio_for_each_segment(bvec, bio, i) { //while循环,每次取出bio中的一个bio_vec来传输
141                 char *buffer = __bio_kmap_atomic(bio, i, KM_USER0); //获取bv_page的内核virtual address
143                 sbull_transfer(dev, sector, bio_cur_sectors(bio), //bio_cur_sectors(bio)求出bio当前segment的大小(sector数目)
144                                 buffer, bio_data_dir(bio) == WRITE);
145                 sector += bio_cur_sectors(bio); //累进数据在硬件上的位置(sector)
147                 __bio_kunmap_atomic(bio, KM_USER0);
149         }
151         return 0; /* Always "succeed" */
152 }

11、块设备的其它操作接口fops(open、release、media_change、revalidate_disk、ioctl)

1) open与release(本驱动可以模拟光盘从光驱中更换)

216 static int sbull_open(struct inode *inode, struct file *filp)
217 {
218         struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data; //inode->i_bdev->bd_disk指向关联的gendisk structure
223         if (! dev->users)
224                 check_disk_change(inode->i_bdev); //check_disk_change将导致对media_change的调用,若介质已改变,将导致调用revalidate_disk
225         dev->users++;
228 }

  • media_changed不为NULL时,则会被内核API check_disk_change所调用;
  • 在media_changed返回true的情况下, revalidate_disk会被内核API check_disk_change所调用

int check_disk_change(struct block_device *bdev)
{
    struct gendisk *disk = bdev->bd_disk;
    struct block_device_operations * bdops = disk->fops;
    if (!bdops->media_changed) //media_changed为NULL
        return 0;
    if (!bdops->media_changed(bdev->bd_disk)) //media_changed不为NULL时,则会被内核API check_disk_change所调用
        return 0;
    if (__invalidate_device(bdev))
        printk("VFS: busy inodes on changed media.\n");
    if (bdops->revalidate_disk) // 在media_changed返回true的情况下,revalidate_disk不为NULL时,则会被内核API check_disk_change所调用
        bdops->revalidate_disk(bdev->bd_disk);
    if (bdev->bd_disk->minors > 1)
        bdev->bd_invalidated = 1;
    return 1;
}

230 static int sbull_release(struct inode *inode, struct file *filp)
231 {
232         struct sbull_dev *dev = inode->i_bdev->bd_disk->private_data;
235         dev->users–;
244 }

12、media_change与revalidate_disk

  • media_changed不为NULL时,则会被内核API check_disk_change所调用
    • 若磁盘介质已更改,应返回true;否则返回false
  • 在media_changed返回true的情况下, revalidate_disk会被内核API check_disk_change所调用
    • 应完成对新磁盘介质进行操作的准备工作

249 int sbull_media_changed(struct gendisk *gd)
250 {
251         struct sbull_dev *dev = gd->private_data;
253         return dev->media_change;
254 }

260 int sbull_revalidate(struct gendisk *gd)
261 {
262         struct sbull_dev *dev = gd->private_data;
264         if (dev->media_change) {
265                 dev->media_change = 0;
266 //              memset (dev->data, 0, dev->size);
267         }
269 }

13、ioctl

  • 大部分的ioctl命令都已经被block layer层所截获并处理,到达不了驱动程序
  • 驱动ioctl函数中需要处理的命令是HDIO_GETGEO (用户,例如fdisk,要求获得磁盘的几何参数)(测试结果似乎OS并未调用ioctl,原因待查)

291 int sbull_ioctl (struct inode *inode, struct file *filp,
292                  unsigned int cmd, unsigned long arg)
293 {
294         long size;
295         struct hd_geometry geo;
296         struct sbull_dev *dev = filp->private_data;
298         switch(cmd) {
299             case HDIO_GETGEO:
301                 /*
302                  * Get geometry: since we are a virtual device, we have to make
303                  * up something plausible.  So we claim 16 sectors, four heads,
304                  * and calculate the corresponding number of cylinders.  We set the
305                  * start of data at sector four.
306                  */
307 //              size = dev->size*(hardsect_size/KERNEL_SECTOR_SIZE);
308                 size = dev->size / KERNEL_SECTOR_SIZE;
309                 geo.cylinders = (size & ~0x3f) >> 6;
310                 geo.heads = 4;
311                 geo.sectors = 16;
312                 geo.start = 4;
313                 if (copy_to_user((void __user *) arg, &geo, sizeof(geo)))
314                         return -EFAULT;
315                 return 0;
316         }
318         return -ENOTTY; /* unknown command */
319 }

单击,与作者交流

发表在 技术博客 | 留下评论

网络设备驱动实例1——cs8900

 单击,返回主页,查看更多内容

本文将以真实的网卡cs8900为实例,介绍网卡驱动程序的编写。(单击下载cs8900驱动源码

通过“网络设备驱动基础”一文,我们学习了网卡驱动编写的大致框架。对于真实网卡驱动,其驱动程序架构也是类似的,最大区别在于:

  • 定义网卡使用的资源(I/O基址和中断号等);在模块的加载函数中调用内核API注册平台设备和平台设备驱动;在模块的卸载函数中调用内核API注销平台设备驱动和平台设备;将原本在模块初始化函数中实现的网卡初始化代码移动到平台设备驱动的probe函数中,将原本在模块卸载函数中实现的网卡卸载代码移动到平台设备驱动的remove函数中。
    • 对于集成在芯片上的设备通常称为平台设备,内核提供了一个基础架构(其实就是一系列数据结构和内核API)供驱动使用,以简化对平台设备及其驱动的操作(例如:热插拔时自动调用平台设备的驱动的probe和remove函数;调用API获取平台设备的I/O基址和中断号等资源)。网卡一般集成在开发板上,所以在模块的初始化函数中,通常会调用平台设备注册API将网卡(连带它使用的资源)注册为平台设备,而后调用平台设备驱动注册API注册与平台设备关联的平台设备驱动结构体,从而通过平台设备驱动中的probe函数完成网卡设备在操作系统中的注册(以前这是在模块的初始化函数中实现的)。在模块的卸载函数中,会调用注销平台设备驱动的API(这将导致平台设备驱动的remove运行,在该函数中通常会有以前在模块的卸载函数中执行的任务——将网卡设备从操作系统中注销),会调用注销平台设备的API。
  • 在网卡驱动的功能函数中,会增加对硬件进行操作的代码。主要有:
    • 注册、释放中断。在open、release中实现
    • 启用和禁用物理网卡。在open、release中实现
    • 物理上初始设置或改变网卡MAC地址。在open、set_mac_address中实现
    • 物理上启用或禁用网卡混杂模式。在cs8900_set_receive_mode中实现
    • 向物理网卡提交发送数据。在cs8900_start_xmit中实现
    • 从物理网卡查询接收到的数据的长度、从物理网卡读取数据。在cs8900_receive中实现
    • 从物理网卡中获得中断类型等。在中断处理程序中实现
  • 要对物理网卡进行上面的物理操作,需要知道物理网卡的寄存器地址和中断号,这2者都是由网卡与CPU的物理连接决定的。因此驱动编写者还必须要能看懂网卡的硬件连线图(单击下载),同时配合查看网卡芯片生产商给出的网卡硬件手册(单击下载),才能知道物理网卡的寄存器地址和中断号是多少

一、驱动整体框架分析

1、驱动接口简介

  • 上层接口
    • 用户接口
      • 网络设备:ethx
      • 用户空间程序
        • ifconfig:直接调用open、close、ioctl接口
        • socket程序:不直接调用send和recv接口,而是将请求提交OS的网络协议栈
    • 操作系统接口
      • 网络协议栈决定发送数据时,调用驱动中的.ndo_start_xmit(skb,ndev)发送函数向硬件发送数据
        • ndo_start_xmit通过内核api——netif_stop_queue (ndev)禁止网络协议栈再度调用ndo_start_xmit
        • ndo_start_xmit向硬件提交数据后立即返回
        • 硬件在完成发送后,以中断的方式通知OS,中断服务程序通过内核api——netif_wake_queue (ndev)通知协议栈可再度调用ndo_start_xmit
      • 硬件接收到数据后,以中断的方式通知OS;中断服务程序调用驱动中的接收函数,该函数在从网卡硬件接收了数据后,调用内核api——netif_rx (skb),向网络协议栈提交数据
  • 下层接口
    • 驱动的.ndo_start_xmit(skb, net_dev)通过写网卡的硬件数据寄存器,将skb中的数据发送给硬件;
    • 中断服务程序通过读取网卡的硬件状态寄存器,判别是接收中断还是发送中断;
    • 驱动的接收函数,通过读网卡的硬件数据寄存器,将数据读入内存,存放于kmalloc的skb中

2、网络设备结构体struct net_device在操作系统内部代表一张网卡,含有很多重要字段

  • name:网卡名称,例如eth0
  • base_addr:I/O基地址
  • irq:中断号
  • dev_addr[]:MAC地址
  • if_port:接口类型
  • features:接口标志
    • IFF_UP
    • IFF_PROMISC
    • IFF_MULTICAST
  • trans_start:发送开始时间
  • last_rx:接收结束时间
  • watchdog_timeo:发送超时时间
  • net_device_ops:包含各个驱动函数指针的结构体
    • open
    • stop
    • hard_start_xmit
    • tx_timeout
    • get_stats
    • do_ioctl
    • set_multicast_list
    • set_mac_address
  • priv:私有数据。由用户定义,通常包括
    • struct net_device_stats stats:记录各种统计信息
    • spinlock_t lock

3、数据包结构体,代表要发送或发送的一个数据包。含有很多重要字段:

  • mac、nh、h:MAC、IP、TCP头指针
  • head、end:数据缓冲区首、尾指针
  • data、tail:数据包有效数据首、尾指针
  • len:数据包长度
  • protocol:协议(ip、ipx等)
  • pkt_type:PACKET_HOST(给我的)、 PACKET_OTHERHOST(不是给我的)、PACKET_BRODCAST、PACKET_MULTICAST

4、网络设备注册(cs8900_probe (struct platform_device *pdev) )

1)操作系统接口

  • ndev = alloc_etherdev(sizeof(struct cs8900_priv));//kmalloc了含priv数据在内的网络设备结构体
  • SET_NETDEV_DEV(ndev, &pdev->dev);//生成sys链接
  • ndev->dev_addr[0] = 0x08;
  • ndev->if_port = IF_PORT_10BASET;
  • priv = netdev_priv(ndev);//获取priv数据指针
  • priv->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0);
  • priv->irq_res= platform_get_resource(pdev, IORESOURCE_IRQ, 0);
  • priv->addr_req = request_mem_region(priv->addr_res->start, iosize,pdev->name);
  • priv->io_addr = ioremap(priv->addr_res->start, iosize);
  • ndev->base_addr = (unsigned long)priv->io_addr;
  • ndev->irq = priv->irq_res->start;
  • ndev->netdev_ops = &cs8900_netdev_ops;//驱动中各个实际函数的指针
  • ndev->watchdog_timeo = HZ;
  • register_netdev(ndev);//将网络设备注册进操作系统(即将结构体链入系统链表)

2)硬件接口

  • 内存控制器设置
    • __raw_writel(0x2211d110,S3C2410_BWSCON);
    • __raw_writel(0x1f7c,S3C2410_BANKCON3);
  • 检测是否是cs8900硬件
    • cs8900_read (ndev,PP_ProductID))!=EISA_REG_CODE
    • value = cs8900_read (ndev,PP_ProductID + 2);
    • if (VERSION (value) != CS8900A)
  • 中断引脚4选1
    • cs8900_write (ndev,PP_IntNum,0);
  • 在网卡硬件寄存器中写入mac地址
    • for (i = 0; i < ETH_ALEN; i += 2)
      cs8900_write (ndev,PP_IA + i,ndev->dev_addr[i] | (ndev->dev_addr[i + 1] << 8));

5、网络设备注销(cs8900_drv_remove(struct platform_device *pdev))

  • struct net_device *ndev = platform_get_drvdata(pdev);
  • struct cs8900_priv *priv = netdev_priv(ndev);
  • platform_set_drvdata(pdev, NULL);
  • unregister_netdev(ndev);//将网络设备结构体从系统链表中摘除
  • cs8900_release_priv(pdev, priv);
    • iounmap(priv->io_addr);
    • release_resource(priv->addr_req);
    • kfree(priv->addr_req);
  • free_netdev(ndev);//kfree网络设备结构体(包括priv结构体)

6、关于平台设备

1)平台设备及其驱动的数据结构定义

  • 单个平台设备的资源(I/O基址、中断号)定义

static struct resource cs8900_resource[] = {
    [0] = {
       .start = 0x19000300,
        .end   = 0x19000310,
        .flags = IORESOURCE_MEM,
    },
    [1] = {
        .start = IRQ_EINT9,
        .end   = IRQ_EINT9,
        .flags = IORESOURCE_IRQ,
    }
};

  • 单个平台设备定义

struct platform_device net_device_cs8900 = {
    .name = "cs8900",
    .id = -1,
    .num_resources =ARRAY_SIZE(cs8900_resource),
    .resource      = cs8900_resource,
};

  • 一组平台设备定义

static struct platform_device *network_devices[] __initdata = {
    &net_device_cs8900,
};

  • 单个平台设备驱动的定义

static struct platform_driver cs8900_driver = {
    .driver    = {
       .name    = "cs8900",
        .owner  = THIS_MODULE,
    },
   .probe   = cs8900_probe,
    .remove  = __devexit_p(cs8900_drv_remove)
}

2)平台设备及其驱动注册

  • 将一组平台设备注册到操作系统中(即:将一组平台设备中的全部设备节点链入操作系统中的平台设备链表中)

platform_add_devices(network_devices, ARRAY_SIZE(network_devices));

  • 将平台设备驱动注册到操作系统中(即:将平台驱动节点链入操作系统中的平台设备驱动链表中,如果能匹配平台设备,就回调平台设备驱动中定义的probe函数)

platform_driver_register(&cs8900_driver)
    {
        将cs8900_driver插入平台驱动链表;
        扫描平台驱动链表以及平台设备链表,用2者的name字段进行匹配;
        if (匹配成功)
            回调平台驱动中定义的probe函数
    }

在注册成功后,如果发生平台设备的热插拔,操作系统会自动回调匹配的平台驱动的probe和remove函数。platform_driver_unregister的调用也会引起平台驱动的remove函数被执行。这样就可以在probe函数中实现网卡设备的注册,在remove函数中实现网卡设备的注销

  • 使用平台设备

在平台设备注册成功后,就可以在任何时候(通常在平台驱动的probe函数中)方便的获得平台设备的信息(资源)

cs8900_probe (struct platform_device *pdev)
{
  priv->addr_res = platform_get_resource(pdev, IORESOURCE_MEM, 0); //priv->addr_res 将会等于cs8900_resource[0],其中有I/O基址0x19000300
  priv->irq_res= platform_get_resource(pdev, IORESOURCE_IRQ, 0);  // priv->irq_res将会等于cs8900_resource[1],其中有中断号IRQ_EINT9
}

7、打开网卡设备cs8900_open(struct net_device *ndev)

  • 被调用时机:ifconfig up
  • 硬件相关操作 /* enable the ethernet controller */参阅代码详细分析以及网卡芯片硬件手册4.4.5-4.4.23;4.10
    • cs8900_set (ndev,PP_RxCFG,RxOKiE | BufferCRC | CRCerroriE | RuntiE | ExtradataiE);
    • cs8900_set (ndev,PP_RxCTL,RxOKA | IndividualA | BroadcastA);
    • cs8900_set (ndev,PP_TxCFG,TxOKiE | Out_of_windowiE | JabberiE);
    • cs8900_set (ndev,PP_BufCFG,Rdy4TxiE | RxMissiE | TxUnderruniE | TxColOvfiE | MissOvfloiE);
    • cs8900_set (ndev,PP_LineCTL,SerRxON | SerTxON);
    • cs8900_set (ndev,PP_BusCTL,EnableRQ);
  • 操作系统相关:注册中断、通知协议栈可启动输出
    • set_irq_type(ndev->irq, IRQ_TYPE_EDGE_RISING);
    • request_irq (ndev->irq, cs8900_interrupt, 0, ndev->name, ndev);
    • netif_start_queue (ndev);

8、关闭网卡设备cs8900_close(struct net_device *ndev)

  • 被调用时机:ifconfig down
  • 硬件相关操作
    • cs8900_write (ndev,PP_BusCTL,0);
    • cs8900_write (ndev,PP_TestCTL,0);
    • cs8900_write (ndev,PP_SelfCTL,0);
    • cs8900_write (ndev,PP_LineCTL,0);
    • cs8900_write (ndev,PP_BufCFG,0);
    • cs8900_write (ndev,PP_TxCFG,0);
    • cs8900_write (ndev,PP_RxCTL,0);
    • cs8900_write (ndev,PP_RxCFG,0);
  • 系统相关操作
    • free_irq (ndev->irq,ndev);
    • netif_stop_queue (ndev);

9、数据发送cs8900_start_xmit (struct sk_buff *skb, struct net_device *ndev)

  • 被调用时机:协议栈想发送数据,且发送队列被激活

netif_stop_queue (ndev);//通知协议栈不要再调用自己发送数据
//通知硬件准备发送以及要发送的数据的长度后,向硬件提交数据 4.10.8
cs8900_write (ndev,PP_TxCMD,TxStart (After5));
cs8900_write (ndev,PP_TxLength,skb->len);
status = cs8900_read (ndev,PP_BusST);
if ((status & TxBidErr)) return 1;//数据包超长,返回1告知协议栈传输错误
if (!(status & Rdy4TxNOW)) return 1;//硬件未能成功分配容纳数据包的buffer
cs8900_frame_write (ndev,skb);//向硬件提交数据
ndev->trans_start = jiffies;//记录发送时间,供协议栈检测超时使用
dev_kfree_skb (skb);//已发送成功,故kfree skb
priv->txlen = skb->len;//暂存发送数据的长度,供将来硬件发送数据成功后,中断服务程序更新统计信息
return 0;//通知协议栈发送成功,协议栈将会将skb从系统链表中摘除,不再重传该skb

  • 如果硬件最终发送成功,将在中断服务程序中netif_wakeup_queue
  • 如果硬件未能成功发送,或由于此处返回1,则由于没有netif_wakeup_queue,协议栈不会再提交发送数据。这将导致最终协议栈发现超时,而调用.ndo_tx_timeout

10、数据发送超时处理cs8900_tx_timeout (struct net_device *ndev)

  • 被调用时机:协议栈发现发送已经超时的时候

struct cs8900_priv *priv = netdev_priv(ndev);
//更新统计记录
priv->stats.tx_errors++;
priv->stats.tx_heartbeat_errors++;
//通知协议栈可重新提交发送数据
netif_wake_queue (ndev);

11、中断处理irqreturn_t cs8900_interrupt (int irq,void *id)

读取硬件的中断类型指示寄存器,判定中断类型。若是发送成功中断则更新统计信息后netif_wakeup_queue;若是接收成功中断则调用接收函数cs8900_receive 。

while ((status = cs8900_read (ndev, PP_ISQ))) {
    switch (RegNum (status)) {
    case TxEvent:
        if (RegContent (status) & TxOK) {
            priv->stats.tx_packets++;
            priv->stats.tx_bytes += priv->txlen;
            netif_wake_queue (ndev);
            break;
        }
    case RxEvent:
        cs8900_receive (ndev);
        break;
    }
}

12、数据接收处理cs8900_receive (struct net_device *ndev)

status = cs8900_read (ndev,PP_RxStatus);
length = cs8900_read (ndev,PP_RxLength);//从硬件获知数据长度
if (!(status & RxOK)) {
    priv->stats.rx_errors++;
    return;
}
skb = dev_alloc_skb (length + 4);//kmalloc skb
skb->dev = ndev;
skb_reserve (skb,2);//因mac头长度为14,+2可保证ip头16字节对齐,方便协议栈处理skb
cs8900_frame_read (ndev,skb,length);//从硬件读取数据,填充skb
skb->protocol = eth_type_trans (skb,ndev);//设置正确的skb->mac\ pkt_type\protocol,去掉mac头
netif_rx (skb);//向协议栈提交skb

13、改变接收模式cs8900_set_receive_mode (struct net_device *ndev)

  • 被调用时机:ifconfig改变接口标志时
  • 功能:启用(或禁用)网卡硬件的混杂、多播模式

if ((ndev->flags & IFF_PROMISC))
  cs8900_set (ndev,PP_RxCTL,PromiscuousA);
else
  cs8900_clear (ndev,PP_RxCTL,PromiscuousA);
if ((ndev->flags & IFF_ALLMULTI) && ndev->mc_list)
  cs8900_set (ndev,PP_RxCTL,MulticastA);
else
  cs8900_clear (ndev,PP_RxCTL,MulticastA);

14、获取统计信息cs8900_get_stats(struct net_device *ndev)

  • 调用时机:ipconfig获取统计信息时

struct cs8900_priv *priv = netdev_priv(ndev);
struct net_device_stats *stats = &priv->stats;
return stats;

15、改变mac地址int (*set_mac_address)(struct net_device *dev, void *addr)

  • 调用时机:ifconfig eth0 hw ether xx:xx:xx:xx:xx:xx
  • 此方法默认被置为eth_mac_addr
    • eth_mac_addr只在接口处于down的情况才执行操作
    • eth_mac_addr只完成将新mac地址addr拷贝到dev->dev_addr中这1个操作
    • 因此驱动如需支持改变mac的操作,需要在驱动的open方法中将硬件的mac地址寄存器的值设为dev->dev_addr中的值(参见open硬件控制代码解析)
  • 若驱动想要彻底支持更改mac地址的操作,需要实现自己的set_mac_address方法
    • 修改dev->dev_addr(给ifconfig看以及作为arp的响应)
    • 修改硬件的mac地址寄存器的值(使硬件获得修改后的mac地址)
    • 修改E2PROM中存储的MAC地址(使修改永久有效)

二、驱动中关于cs8900硬件操作的探讨

一中的内容大多数与“网络设备驱动基础”一文中的内容一致,都比较容易理解。但其中有关硬件的操作,则是与cs8900网卡芯片紧密相关的,要理解与硬件相关的代码,必须了解如何访问和操纵硬件,这要求全面了解网卡芯片的操控机制及各个硬件寄存器的地址与访问方式(这需要阅读网卡芯片数据手册),此外还需要看懂网卡芯片与CPU的硬件连线图。下面对这个问题,予以简要说明。

(画图)cs8900提供2类寄存器:可以用I/O内存地址直接访问的外部寄存器8个;不能用I/O内存地址直接访问,而必须通过外部寄存器间接访问的内部寄存器若干。

1、网卡寄存器的I/O内存地址范围为何是0x19000300 — 0x19000310?

  1. CPU的nGCS3连接CS8900的nCHIPSEL管脚(此管脚必须有效,才能选中cs8900)。当地址为0x18000000时,nGCS3有效
  2. CPU的nGCS3连接CS8900的AEN管脚。当cs8900启用为io模式时(而不是内存模式),AEN必须为低电平。当地址为0x18000000时,nGCS3为低电平
  3. CPU的addr24反相后与LnOE(LnWE)相或,其输出接在了CS8900的nIOR(nIOW)的管脚上。addr24必须为1,cs8900的nIOR(nIOW)才有机会有效
  4. cs8900硬件已经确定了第一个I/O寄存器的地址偏移量为0x300
  5. cs8900硬件共提供8个可以用I/O内存地址直接访问的外部寄存器(它们的地址连续,每个寄存器大小为2个字节)

查看cs8900芯片与CPU的硬件连线图可知1、2、3,再加上4,可得网卡寄存器的I/O内存地址的首地址是0x19000300,再由5可知网卡寄存器的I/O内存地址的末地址是0x19000310

    2、如何访问内部寄存器

    每个内部寄存器都有一个编号。要想访问某个内部寄存器,先将内部寄存器编号写入外部寄存器PP_Address(地址:0x0a),再读取(或写入)外部寄存器PP_Data(地址:0x0c)即可。例如:

    120 static inline u16 cs8900_read (struct net_device *ndev,u16 reg)
    121 {
    122         outw (reg,ndev->base_addr + PP_Address);
    123         return (inw (ndev->base_addr + PP_Data));
    124 }
    125
    126 static inline void cs8900_write (struct net_device *ndev,u16 reg,u16 value)
    127 {
    128         outw (reg,ndev->base_addr + PP_Address);
    129         outw (value,ndev->base_addr + PP_Data);
    130 }
    131
    132 static inline void cs8900_set (struct net_device *ndev,u16 reg,u16 value)
    133 {
    134         cs8900_write (ndev,reg,cs8900_read (ndev,reg) | value);
    135 }
    136
    137 static inline void cs8900_clear (struct net_device *ndev,u16 reg,u16 value)
    138 {
    139         cs8900_write (ndev,reg,cs8900_read (ndev,reg) & ~value);
    140 }

    3、驱动的各个功能函数中操控硬件的代码解析

    1)cs8900_probe中的硬件操控代码

    • 内部寄存器PP_ProductID存放了网卡芯片版本号信息,所以547-557是通过该寄存器来确认芯片是否是cs8900A;
    • cs8900芯片有4个中断管脚,查阅硬件连线图可知:cs8900芯片的第1个中断管脚连接到了CPU。559行将0写入PP_IntNum寄存器,表示选择cs8900的第1个中断管脚来输出中断信号;
    • PP_IA寄存器存放的是网卡运行时的MAC地址,所以565-566行将net_device中的MAC地址写入该寄存器。这一步必须要做,因为本开发板的cs8900芯片没有配置EEPROM来固化MAC地址,因此就必须在网卡被使用前,将指定的MAC地址写入硬件寄存器PP_IA

    547         if ((value = cs8900_read (ndev,PP_ProductID)) != EISA_REG_CODE) {
    548                 printk (KERN_ERR "%s: incorrect signature 0x%.4x\n",ndev->name,value);
    549                 return -ENXIO;
    550         }
    554         if (VERSION (value) != CS8900A) {
    555                 printk (KERN_ERR "%s: unknown chip version 0x%.8x\n",ndev->name,VERSION (value));
    556                 return -ENXIO;
    557         }

    559         cs8900_write (ndev,PP_IntNum,0);

    565         for (i = 0; i < ETH_ALEN; i += 2)
    566                 cs8900_write (ndev,PP_IA + i,ndev->dev_addr[i] | (ndev->dev_addr[i + 1] << 8));

    2)、打开网卡设备cs8900_open(struct net_device *ndev) 中的硬件操控代码

    • 397-398将MAC地址写入PP_IA寄存器,这样一来就可以保证用户能用ifconfig改变网卡的运行时MAC地址(参见稍后的验证);
    • 401行设置PP_RxCFG寄存器(查阅cs8900硬件手册P52),
      • RxOKiE When set, there is an RxOK Interrupt if a frame is received without errors. RxOK interrupt is
        not generated when DMA mode is used for frame reception. 正确接收数据帧则产生中断
      • BufferCRC When set, the received CRC is included with the data stored in the receive-frame buffer, and
        the four CRC bytes are included in the receive-frame length (PacketPage base + 0402h). When
        clear, neither the receive buffer nor the receive length include the CRC. 将CRC校验信息保留在数据帧中
      • CRCerroriE When set, there is a CRCerror Interrupt if a frame is received with a bad CRC. 接收数据帧CRC校验错则产生中断
      • RuntiE When set, there is a Runt Interrupt if a frame is received that is shorter than 64 bytes. The
        CS8900A always discards any frame that is shorter than 8 bytes. 接收数据帧短于64字节则产生中断
      • ExtradataiE When set, there is an Extradata Interrupt if a frame is received that is longer than 1518 bytes.
        The operation of this bit is independent of the received packet integrity (good or bad CRC). 接收数据帧长于1518则产生中断
    • 402行设置PP_RxCTL寄存器(查阅cs8900硬件手册P54),按此设置,将不会启用混杂模式
      • RxOKA When set, the CS8900A accepts frames with correct CRC and valid length (valid length is: 64
        bytes <= length <= 1518 bytes). 要接收CRC正确且有正确长度的帧
      • IndividualA When set, receive frames are accepted if the Destination Address matches the Individual Address
        found at PacketPage base + 0158h to PacketPage base + 015Dh. 要接收发给自己的帧
      • BroadcastA When set, receive frames are accepted if the Destination Address is FFFF FFFF FFFFh. 要接收广播帧
    • 403行设置PP_TxCFG寄存器(查阅cs8900硬件手册P55)
      • TxOKiE When set, an interrupt is generated if a packet is completely transmitted. 成功传送1个数据帧则产生中断
      • Out-of-windowiE When set, an interrupt is generated if a late collision occurs (a late collision is a collision which
        occurs after the first 512 bit times). When this occurs, the CS8900A forces a bad CRC and terminates
        the transmission 发送数据帧的后部(发送512bit之后)时产生冲突,则产生中断(这说明发送时间窗有问题。因为如果局域网上所有机器都严格遵守以太网规范的话,如果有冲突的话,应该在前512bit的时间内就能检测到)
      • JabberiE When set, an interrupt is generated if a transmission is longer than approximately 26 ms. 发送数据帧的时长超过26ms则产生中断
    • 404行设置PP_BufCFG寄存器(查阅cs8900硬件手册P58)
      • Rdy4TxiE When set, there is an interrupt when the CS8900A is ready to accept a frame from the host for
        transmission. (See Section 5.7 on page 98 for a description of the transmit bid process.) 硬件可以接收驱动提交数据,就产生中断
      • RxMissiE When set, there is an interrupt if one or more received frames is lost due to slow movement of
        receive data out of the receive buffer (called a receive miss). When this happens, the RxMiss
        bit (Register C, BufEvent, Bit A) is set. 因硬件接收缓冲区满而造成接收数据包丢失,就产生中断
      • TxUnderruniE When set, there is an interrupt if the CS8900A runs out of data before it reaches the end of the
        frame (called a transmit underrun). When this happens, event bit TXUnderrun (Register C,
        BufEvent, Bit 9) is set and the CS8900A makes no further attempts to transmit that frame. If the
        host still wants to transmit that particular frame, the host must go through the transmit request
        process again. 要传输的数据帧长度小于指示的长度,就产生中断
      • TxColOvfiE If set, there is an interrupt when the TxCOL counter increments from 1FFh to 200h. (The TxCOL
        counter (Register 18) is incremented whenever the CS8900A sees that the RXD+/RXD- pins
        (10BASE-T) or the CI+/CI- pins (AUI) go active while a packet is being transmitted.)  在发送期间有数据到达,这种情况累计发生0x200次,就产生中断(这说明网络很繁忙)
      • MissOvfloiE If MissOvfloiE is set, there is an interrupt when the RxMISS counter increments from 1FFh to
        200h. (A receive miss is said to have occurred if packets are lost due to slow movement of receive
        data out of the receive buffers. When this happens, the RxMiss bit (Register C, BufEvent,
        Bit A) is set, and the RxMISS counter (Register 10) is incremented.) 当RxMissE错误产生累计超过0x200次的时候,就产生中断(这说明硬件数据帧缓冲区太小,或者驱动接收数据的速度太慢)
    • 405行设置PP_LineCTL寄存器(查阅cs8900硬件手册P62)
      • SerRxON When set, the receiver is enabled. When clear, no incoming packets pass through the receiver.
        If SerRxON is cleared while a packet is being received, reception is completed and no subsequent
        receive packets are allowed until SerRxON is set again. 启用接收
      • SerTxON When set, the transmitter is enabled. When clear, no transmissions are allowed. If SerTxON is
        cleared while a packet is being transmitted, transmission is completed and no subsequent
        packets are transmitted until SerTxON is set again. 启用发送
    • 406行设置PP_BusCTL寄存器(查阅cs8900硬件手册P66)
      • EnableRQ When set, the CS8900A will generate an interrupt in response to an interrupt event
        (Section 5.1). When cleared, the CS8900A will not generate any interrupts 启用中断
    • 412行延时一段时间,以使网卡硬件能有时间处理完上述要完成的设置

    如果以上针对硬件的描述你理解不了的话,哈哈,你有空的时候就应该去了解一下ISO的OSI模型的数据链路层的相关知识了。

    391 static int cs8900_open(struct net_device *ndev)
    392 {
    395         /* in case of ifconfig modify mac address */
    397         for (i = 0; i < ETH_ALEN; i += 2)
    398                 cs8900_write (ndev,PP_IA + i,ndev->dev_addr[i] | (ndev->dev_addr[i + 1] << 8));
    400         /* enable the ethernet controller */
    401         cs8900_set (ndev,PP_RxCFG,RxOKiE | BufferCRC | CRCerroriE | RuntiE | ExtradataiE);
    402         cs8900_set (ndev,PP_RxCTL,RxOKA | IndividualA | BroadcastA);
    403         cs8900_set (ndev,PP_TxCFG,TxOKiE | Out_of_windowiE | JabberiE);
    404         cs8900_set (ndev,PP_BufCFG,Rdy4TxiE | RxMissiE | TxUnderruniE | TxColOvfiE | MissOvfloiE);
    405         cs8900_set (ndev,PP_LineCTL,SerRxON | SerTxON);
    406         cs8900_set (ndev,PP_BusCTL,EnableRQ);
    412         udelay(200);
    423 }

    验证用户可以用ifconfig改变网卡的运行时MAC地址:

    开发板 Linux机器

    # ifconfig
    eth0      Link encap:Ethernet  HWaddr 08:00:3E:26:0A:5B
              inet addr:192.168.2.17  Bcast:192.168.2.255  Mask:255.255.255.0

    # ping 192.168.2.11
    PING 192.168.2.11 (192.168.2.11): 56 data bytes
    64 bytes from 192.168.2.11: seq=0 ttl=64 time=3.932 ms

    dennis@dennis-desktop:/$ arp -a
    ? (192.168.2.17)at 08:00:3E:26:0A:5B [ether] on eth3

    # ifconfig eth0 down
    cs8900: close…
    # ifconfig eth0 hw ether 08:00:3E:26:0A:5D
    # ifconfig eth0 up
    # ifconfig
    eth0      Link encap:Ethernet  HWaddr 08:00:3E:26:0A:5D
              inet addr:192.168.2.17  Bcast:192.168.2.255  Mask:255.255.255.0

    # ping 192.168.2.11
    PING 192.168.2.11 (192.168.2.11): 56 data bytes
    64 bytes from 192.168.2.11: seq=0 ttl=64 time=4.612 ms

    dennis@dennis-desktop:/$ arp -a
    ? (192.168.2.17) at 08:00:3E:26:0A:5D [ether] on eth3

     

    3)、关闭网卡设备cs8900_close(struct net_device *ndev) 中的硬件操控代码

    425 static int cs8900_close(struct net_device *ndev)
    426 {
    428         /* disable ethernet controller */
    429         cs8900_write (ndev,PP_BusCTL,0); //禁止产生中断
    430         cs8900_write (ndev,PP_TestCTL,0);
    431         cs8900_write (ndev,PP_SelfCTL,0);
    432         cs8900_write (ndev,PP_LineCTL,0); //禁止发送和接收
    433         cs8900_write (ndev,PP_BufCFG,0);
    434         cs8900_write (ndev,PP_TxCFG,0);
    435         cs8900_write (ndev,PP_RxCTL,0);
    436         cs8900_write (ndev,PP_RxCFG,0);
    443 }

    4)、发送数据函数中的硬件操控代码

    • 273行告知硬件芯片:hi,你好,我要向你提交发送数据了!
    • 274行告知硬件芯片:小子,我要发送的数据帧的长度为skb->len
    • 276行读取硬件芯片的状态
    • 278-285行在硬件芯片拒绝了本次传送请求的情况下,放弃本次传送请求并更新统计信息
    • 287-294行在硬件芯片暂时未准备好接收本次传送请求的情况下(例如:暂时在硬件芯片内部找不到足够空闲空间来容纳本次请求传送的数据),放弃本次请求并更新统计信息。注:由于硬件芯片只是暂时没有空间,所以当稍后有空间了,硬件芯片将产生中断。所以如果你是追求完美的人的话,可以将skb暂存起来,然后在中断处理程序中提交这个数据帧。
    • 296行将要发送的数据帧提交给网卡硬件(如何操作,见稍后分析)

    264 static int cs8900_start_xmit (struct sk_buff *skb,struct net_device *ndev)
    265 {
    273         cs8900_write (ndev,PP_TxCMD,TxStart (After5));
    274         cs8900_write (ndev,PP_TxLength,skb->len);
    276         status = cs8900_read (ndev,PP_BusST);
    278         if ((status & TxBidErr)) {
    281                 priv->stats.tx_errors++;
    282                 priv->stats.tx_aborted_errors++;
    283                 priv->txlen = 0;
    284                 return 1;
    285         }
    287         if (!(status & Rdy4TxNOW)) {
    290                 priv->stats.tx_errors++;
    291                 priv->txlen = 0;
    292                 /* FIXME: store skb and send it in interrupt handler */
    293                 return 1;
    294         }
    296         cs8900_frame_write (ndev,skb);
    310 }

    假设要发送的数据长度为125字节,则下面的函数cs8900_frame_write通过向0x19000300这个I/O内存地址连续写入63个半字来完成将要发送的数据(125个有效的字节+1个无效的字节)提交给硬件芯片。哈哈,到此为止,我驱动的任务就完成了。硬件小子,下面就看你的了!

    147 static inline void cs8900_frame_write (struct net_device *ndev,struct sk_buff *skb)
    148 {
    149         outsw (ndev->base_addr,skb->data,(skb->len + 1) / 2);
    150 }

    5)中断处理irqreturn_t cs8900_interrupt (int irq,void *id)中的硬件操控代码

    • 328行读取PP_ISQ寄存器以获得产生中断的原因;
    • 若是由于接收到数据帧而产生中断,则332-334行调用驱动的接收函数进行处理(当然这里也可能是接收到含有错误的数据帧而产生中断。不过不要紧,待会儿接收处理函数会处理出错的情况)
    • 若是由于发送数据帧而产生中断,则336-349行进行处理
      • 337行更新统计信息,累计产生冲突的次数(由于有367-369,此行似乎可以不要。留待以后解决)
      • 338-342行对应传送不成功(时间窗口不对、传送超过26ms),更新统计信息。这种情况下没有调用netif_wake_queue,所以将最终导致cs8900_tx_timeout被协议栈调用,哈哈,清除障碍的工作本来就是该cs8900_tx_timeout完成的,我可不想越权,自寻烦恼!
      • 343-349行对应发送成功的情况
    • 352-356对应硬件缓冲区不足而导致正确接收到的数据帧被迫丢弃的情况。这种情况下,需要更新统计信息(表示missed的帧数)
    • 357-362对应硬件内部在处理要发送的数据帧的时候,就发现数据帧有问题,从而未执行真正发送的情况。这种情况下,应该调用netif_wake_queue,因为这个错误不是硬件或发送链路出问题导致的,所以cs8900_tx_timeout无能为力。
    • 371-375行似乎可以不要(因为有352-356的存在。留待以后解决)

    312 static irqreturn_t cs8900_interrupt (int irq,void *id)
    313 {
    328         while ((status = cs8900_read (ndev, PP_ISQ))) {
    331                 switch (RegNum (status)) {
    332                 case RxEvent:
    333                         cs8900_receive (ndev);
    334                         break;
    336                 case TxEvent:
    337                         priv->stats.collisions += ColCount (cs8900_read (ndev,PP_TxCOL));
    338                         if (!(RegContent (status) & TxOK)) {
    339                                 priv->stats.tx_errors++;
    340                                 if ((RegContent (status) & Out_of_window)) priv->stats.tx_window_errors++;
    341                                 if ((RegContent (status) & Jabber)) priv->stats.tx_aborted_errors++;
    342                                 break;
    343                         } else if (priv->txlen) {
    344                                 priv->stats.tx_packets++;
    345                                 priv->stats.tx_bytes += priv->txlen;
    346                         }
    347                         priv->txlen = 0;
    348                         netif_wake_queue (ndev);
    349                         break;
    351                 case BufEvent:
    352                         if ((RegContent (status) & RxMiss)) {
    353                                 u16 missed = MissCount (cs8900_read (ndev,PP_RxMISS));
    354                                 priv->stats.rx_errors += missed;
    355                                 priv->stats.rx_missed_errors += missed;
    356                         }
    357                         if ((RegContent (status) & TxUnderrun)) {
    358                                 priv->stats.tx_errors++;
    359                                 priv->stats.tx_fifo_errors++;
    361                                 priv->txlen = 0;
    362                                 netif_wake_queue (ndev);
    363                         }
    364                         /* FIXME: if Rdy4Tx, transmit last sent packet (if any) */
    365                         break;
    367                 case TxCOL:
    368                         priv->stats.collisions += ColCount (cs8900_read (ndev,PP_TxCOL));
    369                         break;
    371                 case RxMISS:
    372                         status = MissCount (cs8900_read (ndev,PP_RxMISS));
    373                         priv->stats.rx_errors += status;
    374                         priv->stats.rx_missed_errors += status;
    375                         break;
    376                 }
    377         }
    379 }

    6)数据接收处理cs8900_receive (struct net_device *ndev) 中的硬件操控代码

    • 231行读取寄存器PP_RxStatus以获知接收数据的状况,以供判断接收数据帧是否正确之用
    • 232行读取寄存器PP_RxLength以获知接收到的数据帧长度,以供copy数据帧到skb之用
    • 234-238行对应接收到错误的数据帧(太短、太长、CRC错),并更新相应统计信息
    • 249行将硬件芯片接收到的数据帧copy到skb中(如何实现,参见稍后分析)

    224 static void cs8900_receive (struct net_device *ndev)
    225 {
    231         status = cs8900_read (ndev,PP_RxStatus);
    232         length = cs8900_read (ndev,PP_RxLength);
    234         if (!(status & RxOK)) {
    235                 priv->stats.rx_errors++;
    236                 if ((status & (Runt | Extradata))) priv->stats.rx_length_errors++;
    237                 if ((status & CRCerror)) priv->stats.rx_crc_errors++;
    238                 return;
    239         }
    249         cs8900_frame_read (ndev,skb,length);
    262 }

    假设硬件芯片接收到的数据长度为125字节,则下面的函数cs8900_frame_read通过从0x19000300这个I/O内存地址连续读取63个半字来完成将接收到的数据(125个有效的字节+1个无效的字节)copy到skb。可见地址0x19000300这个寄存器实际上是硬件芯片内部帧缓存的外部读写窗口。

    142 static inline void cs8900_frame_read (struct net_device *ndev,struct sk_buff *skb,u16 length)
    143 {
    144         insw (ndev->base_addr,skb_put (skb,length),(length + 1) / 2);
    145 }

    7)改变接收模式cs8900_set_receive_mode (struct net_device *ndev) 中的硬件操控代码

    • 455-458行通过操作PP_RxCTL寄存器来指令硬件芯片是否启用混杂模式,
    • 460-463行通过操作PP_RxCTL寄存器来指令硬件芯片是否接收多播包

    453 static void cs8900_set_receive_mode (struct net_device *ndev)
    454 {
    455         if ((ndev->flags & IFF_PROMISC))
    456                 cs8900_set (ndev,PP_RxCTL,PromiscuousA);
    457         else
    458                 cs8900_clear (ndev,PP_RxCTL,PromiscuousA);
    460         if ((ndev->flags & IFF_ALLMULTI) && ndev->mc_list)
    461                 cs8900_set (ndev,PP_RxCTL,MulticastA);
    462         else
    463                 cs8900_clear (ndev,PP_RxCTL,MulticastA);
    464 }

    验证用户可以用ifconfig使网卡处于混杂模式:

    去掉255、256行(位于函数cs8900_receive中)的注释,以显示接收到的数据包的类型。

    #define PACKET_HOST        0        /* To us        */
    #define PACKET_BROADCAST    1        /* To all        */
    #define PACKET_MULTICAST    2        /* To group        */
    #define PACKET_OTHERHOST    3        /* To someone else     */

    255         if (((skb->pkt_type) == PACKET_HOST) || ((skb->pkt_type) == PACKET_OTHERHOST))
    256                 printk("received a frame which package type is %d\n", skb->pkt_type);

    开发板 Linux机器

    # ifconfig eth0 192.168.2.17
    # ifconfig eth0 promisc
    device eth0 entered promiscuous mode

     
     

    dennis@dennis-desktop:/$ ping -c 1 192.168.2.17
    PING 192.168.2.17 (192.168.2.17) 56(84) bytes of data.
    64 bytes from 192.168.2.17: icmp_seq=1 ttl=64 time=1.74 ms

    # received a frame which package type is 0  
      dennis@dennis-desktop:/$ sudo arp -s 192.168.2.18 08:00:3E:26:0A:59

    dennis@dennis-desktop:/$ ping -c 1 192.168.2.18
    PING 192.168.2.18 (192.168.2.18) 56(84) bytes of data.

    — 192.168.2.18 ping statistics —
    1 packets transmitted, 0 received, 100% packet loss, time 0ms

    received a frame which package type is 3  

    # ifconfig eth0 -promisc
    device eth0 left promiscuous mode

     
     

    dennis@dennis-desktop:/$ ping -c 1 192.168.2.17
    PING 192.168.2.17 (192.168.2.17) 56(84) bytes of data.
    64 bytes from 192.168.2.17: icmp_seq=1 ttl=64 time=1.74 ms

    # received a frame which package type is 0  
     

    dennis@dennis-desktop:/$ ping -c 1 192.168.2.18
    PING 192.168.2.18 (192.168.2.18) 56(84) bytes of data.

    — 192.168.2.18 ping statistics —
    1 packets transmitted, 0 received, 100% packet loss, time 0ms

    没有任何显示  

    4、中断服务程序为何是while循环?

    • cs8900会同时出现多个能引发中断的事件(例如:在双工模式下,发送数据的同时也接收到了数据),此时cs8900的ISQ寄存器就表明了这多个中断事件
    • 需要用循环来处理全部中断事件

    5、中断为何是EINT9?为什么是上升沿触发?

    76                 .start = IRQ_EINT9,

    414         set_irq_type(ndev->irq, IRQ_TYPE_EDGE_RISING);

    • CPU的EINT9管脚接在了CS8900的INTRQ0上。而CS8900的默认配置(无EEPROM时)启用的是4个中断中的INTRQ0,即使不是这样,我们在程序中也明确指定的是INTRQ0
    • 查cs8900硬件芯片手册可知:中断发生时,INTRQ0从低电平变为高电平

    5、为什么要设置内存控制器?

    500         __raw_writel(0x2211d110,S3C2410_BWSCON);//!!
    501         __raw_writel(0x1f7c,S3C2410_BANKCON3);//!!

    答:cs8900的I/O内存地址占用的是BANK3的地址空间

    • __raw_writel(0x2211d110,S3C2410_BWSCON)
      • 0xd=0b1101,表示bank3的配置为:
        • (0b1)使用nBE,而不是nWBE,即:nWBE/nBE管脚在读写字节时均输出有效信号,而不是仅仅在写字节才输出有效信号。这是由于nWBE1/nBE1管脚接的是cs8900的nSBHE(system bus high enable)管脚,表示16位数据中的高8位有效,在读写字节中均有效,而不仅仅在写字节中有效
        • (0b1)表示enable wait信号,原因不详。CPU的nWAIT信号接的是cs8900的IOCHRDY管脚。
        • (0b01)表示数据位宽16bit。因为cs8900的数据总线位宽为16bit
      • 0x2,是bank6正确的配置
    • __raw_writel(0x1f7c,S3C2410_BANKCON3)
      • bank3的控制寄存器,配置是正确的,但其中的各个参数是什么含义,不明

     

    网络设备驱动框架总结:

    • 两个接口
      • 上层与os的接口
      • 下层与硬件的接口
    • 两个重要数据结构
      • 网络设备结构体
      • socket buffer结构体
    • 四个主要回调函数
      • 初始化函数init
      • 发送数据包函数start_xmit
      • 中断处理函数interrupt handler
      • 接收数据包函数recv

    单击,与作者交流

    发表在 技术博客 | 留下评论

    网络设备驱动基础

    单击,返回主页,查看更多内容 

    一、体验网卡驱动

    1、下载虚拟网卡驱动源码(单击下载)后,执行make得到snull.ko,加载驱动 sudo insmod snull.ko

    2、分别配置2张网卡(sn0和sn1)的ip地址

    dennis@dennis-desktop:/work/studydriver/snull$ sudo ifconfig sn0 192.168.140.1
    dennis@dennis-desktop:/work/studydriver/snull$ sudo ifconfig sn1 192.168.141.2

    3、测试2张网卡之间的通信

    dennis@dennis-desktop:/work/studydriver/snull$ ping 192.168.140.2
    PING 192.168.140.2 (192.168.140.2) 56(84) bytes of data.
    64 bytes from 192.168.140.2: icmp_seq=1 ttl=64 time=0.167 ms
    64 bytes from 192.168.140.2: icmp_seq=2 ttl=64 time=0.112 ms
    64 bytes from 192.168.140.2: icmp_seq=3 ttl=64 time=0.111 ms
    64 bytes from 192.168.140.2: icmp_seq=4 ttl=64 time=0.139 ms

    — 192.168.140.2 ping statistics —
    4 packets transmitted, 4 received, 0% packet loss, time 3000ms
    rtt min/avg/max/mdev = 0.111/0.132/0.167/0.024 ms
    注:也许你感觉2中ip地址的分配和3中的ping的目标有些奇怪,甚至有些难以理解。这是由于我们要测试网卡sn0是否能ping通网卡sn1,而sn0和sn1是本机的2张网卡,如果将它们的ip地址配在同一网段,则测试时数据包根本不会外送。为解决测试的问题,scull驱动对外发数据包作了一些额外处理,将目标ip的第3段加1(即:将目标ip从192.168.140.2改为192.168.141.2),将源ip的第3段加1(即:将源ip从192.168.140.1改为192.168.141.1)。详情请参见《Linux Device Driver》17.1节。

    二、网卡驱动的基本知识——2个结构体和5个函数

    1、结构体 struct net_device

    网络设备的注册方式与字符和块设备不同的。网络设备没有主次设备号,驱动为每个刚刚探测到的网络设备(或称为接口)在一个全局的网络设备链表里插入一个节点,该节点就代表1个网络设备,它是struct net_device类型的结构体。

    1)、net_device结构体的内容(字段)很多,主要用于操作系统和驱动程序操控和查询网卡

    • 全局信息(例如:接口名称eth0)
    • 硬件信息(例如:中断号、I/O base)
    • 接口信息(例如:MAC地址、混杂模式标志)
    • 设备功能函数指针(例如:数据发送函数)
    • 其它字段(例如:priv、自旋锁)

    2) 、net_device结构体的生成与初始化

    struct net_device *alloc_netdev(int sizeof_priv, const char *name, void (*setup)(struct net_device *))

    • sizeof_priv 是驱动的的“私有数据”区的大小
      • 对于网络驱动, 这个区是同 net_device 结构一起分配的(逻辑上可认为“私有数据”区紧随net_device 结构之后)
      • net_device 结构体中有一个成员priv,它就是指向“私有数据”区的指针。它的角色近似于我们用在字符驱动上的 private_data 指针,当一个驱动需要存取私有数据指针, 应当使用 netdev_priv 函数,例如:struct snull_priv *priv = netdev_priv(dev);
    • name是网络设备的名字,例如:eth0、eth1、sn0、sn1
    • setup 是一个初始化函数的指针, 被alloc_netdev调用以初始化 net_device 结构的大部分字段。网络子系统针对alloc_netdev为各种接口提供了一些封装函数,如:
      • 以太网设备:alloc_etherdev
        • struct net_device *alloc_etherdev(int sizeof_priv)这个函数分配一个网络设备结构体,使用 eth%d 作为参数 name。它提供了自己的初始化函数 ( ether_setup )来设置许多 net_device 成员,使用对以太网设备合适的值。没有驱动提供的初始化函数给 alloc_etherdev;
        • 通过 ether_setup 函数(由 alloc_etherdev 调用),内核负责了一些以太网范围中的缺省值
      • 光纤通道设备:alloc_fcdev
      • FDDI 设备: alloc_fddidev
      • 令牌环设备:alloc_trdev

    3)、net_device结构体的注册(插入全局的网络设备链表)

    在对net_device结构体完成初始化后,传递这个结构给 int register_netdev(struct net_device *dev),以完成注册

    4)、将net_device结构体从内核中注销

    void unregister_netdev(struct net_device *dev)

    5)、net_device结构体的销毁

    void free_netdev(struct net_device *dev) 归还 net_device 结构给内核

    2、 网络接口的打开与关闭

    • 当用户执行ifconfig 命令时,内核会打开或者关闭一个接口
    • 打开接口:ifconfig eth0 192.168.10.1 up
      • 通过 ioctl(SIOCSIFADDR)( Socket I/O Control Set Interface Address) 来安排ip地址.—–内核实现
      • 调用dev->open方法——驱动实现
      • 通过 ioctl(SIOCSIFFLAGS) ( Socket I/O Control Set Interface Flags) 来设置 dev->flag 的 IFF_UP 位,. —–内核实现
    • 接口关闭:ifconfig eth0 down
      • 通过 ioctl(SIOCSIFFLAGS) 来清除dev->flag 的 IFF_UP位——内核实现
      • 调用dev->stop 方法——驱动实现
    • 驱动的open方法
      • 把硬件 (MAC) 地址从硬件设备拷贝到 dev->dev_addr
      • 注册中断
      • 启动接口传输队列
        • void netif_start_queue(struct net_device *dev);
      • snull示例

        int snull_open(struct net_device *dev) {
            memcpy(dev->dev_addr, "SNUL0", ETH_ALEN);
            netif_start_queue(dev); 

    • 驱动的stop方法
      • 注销中断
      • 停止接口传输队列
        • void netif_stop_queue(struct net_device *dev);
      • snull示例

        int snull_release(struct net_device *dev) {
            netif_stop_queue(dev);
        }

    3、数据包发送

    • 无论何时内核需要传送一个数据包, 它调用驱动的 hard_start_stransmit 方法将数据放在外发队列上,成功时返回0,不成功返回非0
    • 每个内核处理的数据包都包含在一个 socket 缓存结构( 结构 sk_buff )skb里
      • skb->data 指向要传送的数据包
      • skb->len 指向要传送的数据包的长度
    • 控制发送并发
      • hard_start_xmit 函数由一个 net_device 结构中的自旋锁(xmit_lock)来保护避免并发调用,这样在函数未返回前不会出现对它的并发调用。
      • hard_start_xmit 函数一返回,它有可能被再次调用。当软件完成指示硬件发送数据包后,该函数返回,但此时硬件传送可能还没有完成。因此驱动需要告知协议栈不要再启动发送,直到硬件准备好接收新的数据.
        • 该告知是驱动通过调用 netif_stop_queue 来实现的
        • 在中断中调用void netif_wake_queue(struct net_device *dev)告知协议栈可再次发送数据
        • 如果从其他地方停止数据包的传送,不是 hard_start_xmit 函数,则使用的函数是:void netif_tx_disable(struct net_device *dev);这个函数类似 netif_stop_queue, 但是它还保证, 当它返回时, 你的 hard_start_xmit 方法没有在另一个 CPU 上运行。队列能够用 netif_wake_queue 重启
    • 发送超时解决办法
      • 设置定时器来处理这个问题。不过,网络驱动不需要自己去检测超时。它只需要设置
        • 1个超时值(在 net_device 结构的 watchdog_timeo 成员,以 jiffies 计)
        • 最后1个数据包的发送时间(在net_device 结构的 trans_start成员,以 jiffies 计) 。
      • 如果发送超时,协议栈的网络层最终会发现,并调用驱动的 tx_timeout 方法。这个方法的工作是:
        • 将引起发送超时的故障排除
        • 并且保证任何已经开始的发送正确地完成。特别地, 驱动没有丢失追踪任何协议栈委托给它的 socket 缓存(即:需要发送的数据)

    4、中断处理

    • 硬件芯片(例如:网卡芯片)可能因为3种情况而触发中断:
      • 硬件将一个外发数据包发送完成
      • 一个新数据包到达硬件
      • 网络接口也能够产生中断来指示错误, 例如状态改变
    • 驱动的中断处理程序可以通过检查硬件芯片中的状态寄存器,能够得知是3种触发中断中的哪一种情况,然后
      • 通知协议栈可重新启动发送队列netif_wake_queue(dev);
      • 调用接收函数
      • 处理错误

    5、 数据包接收

    数据包接收有2种模式:中断驱动和轮询。这里只介绍第中断驱动,在这种模式下,数据包接收函数是由中断处理程序来调用的。接收函数完成功能的流程如下:

    1)分配一个缓存区来保存数据包.

    • 缓存分配函数 (dev_alloc_skb) 需要知道数据长度,函数用这些信息来给缓存区分配空间。该信息来源于网卡的硬件寄存器
    • dev_alloc_skb 使用 atomic 优先级调用 kmalloc , 因此它可以在中断时间安全使用

    2)一旦有一个有效的 skb 指针, 通过调用 memcpy, 报文数据被拷贝到缓存区

    • memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);

    3)skb的dev、protocol、pkt_type 成员必须在缓存向上传递前赋值,使协议栈知道数据包的一些信息。以太网支持代码输出一个辅助函数 eth_type_trans(skb, dev) 来完成这个工作

    • 通过skb->mac.raw的协议类型,发现一个合适的protocol值作为该函数的返回值
    • 通过skb->mac.raw的目标mac与dev中的本机mac地址比较,来赋值给skb->pkt_type
      • PACKET_HOST,是发给本机的包
      • PACKET_OTHERHOST,不是发给本机的包
      • PACKET_BROADCAST,广播包
      • PACKET_MULTICAST,多播包
    • 去掉mac头

    4)指出IP校验和要如何进行,即:对skb->ip_summed赋值。其取值有3种:

    • CHECKSUM_HW,硬件已经进行校验
    • CHECKSUM_NONE,未进行校验,需要协议栈进行校验
    • CHECKSUM_UNNECESSARY,不必进行校验(loop接口即是如此)

    5)驱动更新它的统计计数(存放于私有数据区)来记录收到一个报文。统计结构由几个成员组成,最重要的是:

    • rx_packet, 表示收到的报文数目
    • rx_bytes, 表示收到的字节总数
    • tx_bytes, 表示发送的字节总数

    6)调用int netif_rx(struct sk_buff *skb)执行最后的接受数据包工作, 它递交 socket 缓存给上层。netif_rx 返回一个整数:

    • NET_RX_SUCCESS(0) 意思是报文成功接收
    • 任何其他值指示错误
      • NET_RX_CN_LOW, NET_RX_CN_MOD,和 NET_RX_CN_HIGH 指出网络子系统的递增的拥塞级别
      • NET_RX_DROP 意思是报文被丢弃

    6、结构体 struct sk_buff及相关内核API

    1)结构体 struct sk_buff的组成

    struct sk_buff {
        union { /* … */ } h;
        union { /* … */ } nh;
        union { /*… */} mac;
        uchar *head;
        uchar *data;
        uchar *tail;
        uchar *end;
        uint len;//数据包长度
        uint truesize;
        uchar ip_summed;
        uchar pkt_type;
        ushort protocol
    } *skb;
    image

    可用缓存空间是 skb->end – skb->head, 有效数据(即:数据包)的空间是 skb->tail – skb->data

    truesize:表示缓存区的整体长度,置为sizeof(struct sk_buff)加上传入alloc_skb()函数的长度(或dev_alloc_skb分配的数据缓存区长度)

    2)分配Socket 缓存的API(对应的释放API用kfree替换alloc即可)

    • struct sk_buff *alloc_skb(unsigned int len, int priority);

    分配一个缓存区. alloc_skb 函数分配一个缓存并且将 skb->data 和 skb->tail 都初始化成 skb->head.

    • struct sk_buff *dev_alloc_skb(unsigned int len);

    dev_alloc_skb 函数是使用 GFP_ATOMIC 优先级调用 alloc_skb 的快捷方法, 并且在 skb->head 和 skb->data 之间保留了一些空间. 这个数据空间用在网络层之间的优化, 驱动不要动它

    3)操纵Socket 缓存的API

    • unsigned char *skb_put(struct sk_buff *skb, int len);
    • unsigned char *__skb_put(struct sk_buff *skb, int len);

    更新 sk_buff 结构中的 tail 和 len 成员; 它们用来增加数据到缓存的结尾, 每个函数的返回值是 skb->tail 的前一个值(换句话说, 它指向刚刚创建的数据空间). 两个函数的区别在于 skb_put 检查以确认数据适合缓存, 而 __skb_put 省略这个检查.

    • unsigned char *skb_push(struct sk_buff *skb, int len);
    • unsigned char *__skb_push(struct sk_buff *skb, int len);

    递减 skb->data 和递增 skb->len 的函数. 它们与 skb_put 相似, 除了数据是添加到报文的开始而不是结尾. 返回值指向刚刚创建的数据空间. 这些函数用来在发送报文之前添加一个硬件头部. 又一次, __skb_push 不同在它不检查空间是否足够

    • void skb_reserve(struct sk_buff *skb, int len);

    将data 和 tail的值加上len.

    三、snull网卡驱动代码分析

    1、设备生成与注册

    688 int snull_init_module(void)
    689 {
    694         /* Allocate the devices */
    695         snull_devs[0] = alloc_netdev(sizeof(struct snull_priv), "sn%d",
    696                         snull_init);
    697         snull_devs[1] = alloc_netdev(sizeof(struct snull_priv), "sn%d",
    698                         snull_init);
    703         for (i = 0; i < 2;  i++)
    704                 if ((result = register_netdev(snull_devs[i])))
    705                         printk("snull: error %i registering device \"%s\"\n",
    706                                         result, snull_devs[i]->name);
    707                 else
    708                         ret = 0;
    714 }

    在模块初始化函数中,695、697行初始化net_device结构体。struct snull_priv结构体用来存放一些私有数据,例如:统计数据。函数snull_init则设置net_device的各个重要字段。704行并将代表网卡的net_device结构体注册进操作系统。至此,网卡就存在于系统中,可以被使用了。

    模块的卸载函数完成相反的功能——将net_device结构体从内核中注销,以及销毁net_device结构体

    617 void snull_init(struct net_device *dev)
    618 {
    634         dev->open            = snull_open;
    635         dev->stop            = snull_release;
    636         dev->set_config      = snull_config;
    637         dev->hard_start_xmit = snull_tx;
    638         dev->do_ioctl        = snull_ioctl;
    639         dev->get_stats       = snull_stats;
    640         dev->change_mtu      = snull_change_mtu;
    641         dev->rebuild_header  = snull_rebuild_header;
    642         dev->hard_header     = snull_header;
    643         dev->tx_timeout      = snull_tx_timeout;
    644         dev->watchdog_timeo = timeout; //timeout为5个jiffies
    663 }

    snull_init函数将net_device结构体的重要字段初始化为了驱动中实现的各个功能函数,因此

    • 当ifconfig sn0 up打开网卡时,snull_open将被调用
    • 当ifconfig sn1 down关闭网卡时,snull_release将被调用
    • 当ifconfig查看网卡统计数据时,scull_stats将被调用
    • 当传送数据包超过了5个jiffies仍然没有成功的话,snull_tx_timeout将被调用进行善后处理
    • 当协议栈需要发送数据时,snull_tx将被调用

    下面就来分析这几个功能函数。

    2、设备打开、关闭与查询

    scull_open完成:填写struct net_device结构体的dev_addr字段(mac地址);启动传输队列netif_start_queue(dev)

    scull_release完成:停止传输队列netif_stop_queue(dev)

    scull_stats完成:将网卡的统计数据返回给操作系统。之后操作系统将该信息返回给应用程序(例如:ifconfig)

    78 struct snull_priv {
    79         struct net_device_stats stats;
    86         struct sk_buff *skb;
    87         spinlock_t lock;
    88 };

    557 struct net_device_stats *snull_stats(struct net_device *dev)
    558 {
    559         struct snull_priv *priv = netdev_priv(dev);
    560         return &priv->stats;
    561 }

    3、数据的发送

    需要发送数据时,协议栈会调用dev->hard_start_xmit。传入的第1个参数是socket缓存结构体(通过它可以找到需要发送的数据内容、长度等相关信息)的指针;第2个参数是对应于该网卡的net_device结构体。

    • 509行获得实际要发送的数据,510行获得要发送数据的长度
    • 517行在net_device结构体中设置发送开始时间,以便让协议栈能检测发送超时
    • 协议栈一旦调用dev->hard_start_xmit将skb递交给驱动,就不会在协议栈中再保留该skb。因此520行将skb暂存起来,以便将来数据被硬件发送成功后,能在中断处理程序中释放该skb(不在发送函数中释放skb,是因为此时不能保证硬件能发送数据成功)
    • 523行将要发送的数据提交给硬件
    • 由于本程序驱动的是虚拟网卡,所以不会出现硬件发送数据不成功的情况。在真实的情况下,这种状况是有可能发生的,因此真实网卡驱动程序还会在523行调用netif_stop_queue通知协议栈暂停发送数据,直到硬件发送成功产生中断,在中断处理程序中调用netif_wake_queue通知协议栈重新启动发送
    • 525行返回0,表示发送成功。若不为零则表示失败。
    • 注:511–516行是预防协议栈下发的数据包长度未能达到以太网最小数据包的长度(#define ETH_ZLEN    60)

    501 int snull_tx(struct sk_buff *skb, struct net_device *dev)
    502 {
    503         int len;
    504         char *data, shortpkt[ETH_ZLEN];
    505         struct snull_priv *priv = netdev_priv(dev);
    509         data = skb->data;
    510         len = skb->len;
    511         if (len < ETH_ZLEN) {
    512                 memset(shortpkt, 0, ETH_ZLEN);
    513                 memcpy(shortpkt, skb->data, skb->len);
    514                 len = ETH_ZLEN;
    515                 data = shortpkt;
    516         }
    517         dev->trans_start = jiffies; /* save the timestamp */
    519         /* Remember the skb, so we can free it at interrupt time */
    520         priv->skb = skb;
    522         /* actual deliver of data is device-specific, and not shown here */
    523         snull_hw_tx(data, len, dev);
    525         return 0; /* Our simple device can not fail */
    526 }

    如果协议栈检测到发送超时(如果协议栈被netif_stop_queue(dev)通知暂停发送后,超过dev->watchdog_timeo个jiffies仍然没有被netif_wake_queue(dev)通知恢复传送,则协议栈认为在网卡dev上发生了发送超时),将会调用dev->tx_timeout。

    • 533、540行更新网卡统计信息
    • 541行恢复协议栈可以继续向网卡驱动提交数据
    • 注:如果你要追求完美的话,应该在541行之前添加代码调用snull_tx完成重发操作,并在重发前获得dev->xmit_lock自旋锁,重发后释放自旋锁,以避免与协议栈调用dev->hard_start_xmit产生竞态

    531 void snull_tx_timeout (struct net_device *dev)
    532 {
    533         struct snull_priv *priv = netdev_priv(dev);
    540         priv->stats.tx_errors++;
    541         netif_wake_queue(dev);
    543 }

    4、中断处理程序

    • 中断处理程序的注册一般放在驱动的open功能函数中。由于本程序驱动的是虚拟网卡,没有物理中断,所以就没有注册中断的代码。注:本程序使用定时器来模拟中断。
    • 332-336行说明,对于真实硬件而言,应该首先检查是否发生的中断
    • 由于数据包接收程序需要网卡对应的net_device结构体中的一些字段(例如:dev、dev_addr,具体参见接收程序),所以337行去获得它。当然337行要达到目的,要求我们在注册中断时要用网卡对应的net_device结构体指针作为request_irq的第5个参数(这一点不难做到,因为open被调用时,net_device结构体指针是传入参数)
    • 348行表明对于真实设备,此时应该读取硬件获得中断的原因(硬件接收完成?硬件发送完成?硬件错误?),本程序用349行模拟
    • 351-358处理接收中断。注:如果是真实硬件,只需要简单的调用snull_rx(dev)即可
    • 359-364处理发送中断。361-362更新统计信息;由于硬件已经成功发送数据包,所以363行可以放心大胆地释放skb的空间(发送函数曾暂存该skb)

    327 static void snull_regular_interrupt(int irq, void *dev_id, struct pt_regs *regs)
    328 {
    329         int statusword;
    330         struct snull_priv *priv;
    331         struct snull_packet *pkt = NULL;
    332         /*
    333          * As usual, check the "device" pointer to be sure it is
    334          * really interrupting.
    335          * Then assign "struct device *dev"
    336          */
    337         struct net_device *dev = (struct net_device *)dev_id;
    348         /* retrieve statusword: real netdevices use I/O instructions */
    349         statusword = priv->status;
    350         priv->status = 0;
    351        if (statusword & SNULL_RX_INTR) {
    352                 /* send it to snull_rx for handling */
    353                 pkt = priv->rx_queue;
    354                 if (pkt) {
    355                         priv->rx_queue = pkt->next;
    356                        snull_rx(dev, pkt);
    357                 }
    358         }
    359         if (statusword & SNULL_TX_INTR) {
    360                 /* a transmission is over: free the skb */
    361                 priv->stats.tx_packets++;
    362                 priv->stats.tx_bytes += priv->tx_packetlen;
    363                 dev_kfree_skb(priv->skb);
    364         }
    368         if (pkt) snull_release_buffer(pkt); /* Do this outside the lock! */
    370 }

    5、数据的接收

    当硬件成功接收数据后会产生中断,中断处理程序在判明是接收中断后会调用数据包接收程序,并将struct net_device结构体的指针作为参数传递

    • 258行分配skb空间准备接收数据,+2是为265行作准备
    • 259-263行,发现如果不能分配内存则将该数据包丢弃,并更新统计信息
    • 265行将数据包在skb中的存放位置下移2字节,目的是为了让数据包中的ip头开始在16字节对齐的内存位置上,以方便将来协议栈对数据包的处理
    • 266行将接收到的数据拷贝到skb中(同时完成skb内部数据的更新,这由skb_put完成)。注意:对于真实硬件而言,266行将被扩展为对硬件的物理操作代码(需要从硬件获得数据的长度,读取硬件寄存器以获得数据包的内容)
    • 269-271行设置skb的一些字段,以使将来协议栈能获得数据包的一些基础信息。269行设置网卡对应的net_device结构体;270行设置包的目的类型(skb->pkt_type)和协议类型(skb->protocol,作为eth_type_trans的返回值);271行表示协议栈的网络层不需要检查ip包的校验和(注意:loop接口和虚拟网卡可以这样,但物理网卡必须要检查)
    • 272-273更新统计信息
    • 274行将skb上交协议栈处理。哈哈,剩下的事,可就不关我驱动的事了。看你的了,协议栈!

    249 void snull_rx(struct net_device *dev, struct snull_packet *pkt)
    250 {
    251         struct sk_buff *skb;
    252         struct snull_priv *priv = netdev_priv(dev);
    254         /*
    255          * The packet has been retrieved from the transmission
    256          * medium. Build an skb around it, so upper layers can handle it
    257          */
    258         skb = dev_alloc_skb(pkt->datalen + 2);
    259         if (!skb) {
    262                 priv->stats.rx_dropped++;
    263                 goto out;
    264         }
    265         skb_reserve(skb, 2); /* align IP on 16B boundary */
    266         memcpy(skb_put(skb, pkt->datalen), pkt->data, pkt->datalen);
    268         /* Write metadata, and then pass to the receive level */
    269         skb->dev = dev;
    270         skb->protocol = eth_type_trans(skb, dev);
    271         skb->ip_summed = CHECKSUM_UNNECESSARY; /* don’t check it */
    272         priv->stats.rx_packets++;
    273         priv->stats.rx_bytes += pkt->datalen;
    274         netif_rx(skb);
    275   out:
    276         return;
    277 }

    总结:网卡驱动的编写,主要内容是

    • 2个结构体:net_device和struct sk_buff。前者记录网卡设备相关信息,后者保存接收到和要发送的数据
    • 5个函数:打开、关闭、发送、接收、中断。特别地,接收函数不是由协议栈调用的,而是由中断处理程序调用的,并向协议栈提供数据

    单击,与作者交流

    发表在 技术博客 | 留下评论

    Linux中断处理系统的架构与共享中断

    单击,返回主页,查看更多内容

    一、裸机程序中的中断编程与有操作系统下的中断编程的区别

    • 当硬件发生中断时,CPU硬件会自动执行4个操作,从而无条件跳转到异常向量处(参见ARM体系结构与编程中关于异常和中断的内容)
    • 异常向量处放置了1条跳转指令,跳转到中断异常处理程序。编写裸机程序需要想办法将这条指令放置到异常向量处,而操作系统已经帮我们做了
    • 编写裸机程序当然需要编写中断异常处理程序去完成诸如环境保护(CPU寄存器入栈)、获取中断号、中断异常结束返回之类的操作,而操作系统已经帮我们全部都做了(啊,伟大的操作系统!)。然后调用中断处理程序(也就是通常所说的ISR)。
    • 不同硬件的中断处理程序当然肯定不同,所以要求操作系统把这个也帮你写了,实在是太过分了。所以,编写ISR就是驱动编写者的责任了!当然,裸机程序的编写者肯定也要编写ISR
    • 编写ISR,对于裸机程序编写者而言百无禁忌,想怎么搞就怎么搞。但对于有操作系统的情况下则要受到一些限制(当然不能无法无天,否则还要操作系统这个领导来干嘛?),例如:不能睡眠,要用虚地址访问硬件等
    • 特别说明:操作系统在启动过程中会初始化中断子系统,这会在操作系统内部将所有中断的逻辑上的中断号(整数)与物理的中断线进行关联。因此当驱动调用request_irq告知操作系统将中断号与中断处理程序进行关联时,操作系统就能够将物理的中断线与中断处理程序进行关联,从而当某个物理中断产生时,操作系统有能力知道应该回调哪一个中断处理程序。

    一、Linux中断处理系统的架构(详情参阅《嵌入式linux应用开发完全手册》P401-P403)

    • 3种结构:irq_desc、irq_chip、irqaction
    • 发生中断时,CPU执行中断异常向量vector_irq的代码
    • vector_irq最终会调用中断处理的总入口函数asm_do_IRQ
    • asm_do_IRQ会根据中断号为下标来调用irq_desc数组中对应元素中的handle_irq函数
    • handle_irq会使用chip成员中的函数来设置硬件,例如清除中断等
    • handle_irq逐个调用action链表中注册的处理函数
    • 中断处理系统的初始化就是构造irq_desc数组元素中的handle_irq、 chip等成员
    • 用户注册中断就是构造action链表;释放中断就是从action链表中去除不需要的项

    二、关于共享中断的说明

    • 共享同一中断号的ISR,通过request_irq中的同一个中断号注册到系统中的同一个中断链表中,而dev_id则用于区分该中断链表中不同的ISR,因此注册时dev_id不能相同,也不能为NULL
    • 共享中断的所有注册,其第3个参数都必须指明IRQF_SHARED,以表示是注册共享中断
    • 中断发生时,系统会顺次调用中断链表中所有的ISR。因此ISR一定要根据自己是否真的发生了中断,来返回IRQ_HANDLED或IRQ_NONE
    • 释放中断free_irq的第2个参数dev_id,正是用来查找中断链表中要被释放的中断

    三、共享中断实例

    dummyisr.c如下:

    11 static irqreturn_t dummy_isr(int irq, void *dev_id)
    12 {
    13         static int haveint = 0;
    14         if (haveint == 0) {
    15                 printk(KERN_NOTICE "dummy_isr will return IRQ_NONE");
    16                 haveint = 1;
    17                 return IRQ_RETVAL(IRQ_NONE);
    18         } else {
    19                 printk(KERN_NOTICE "dummy_isr will return IRQ_HANDLED");
    20                 haveint = 0;
    21                 return IRQ_RETVAL(IRQ_HANDLED);
    22         }
    23 }
    24
    25 struct cdev dummy_cdev;
    26 static int __init my_init(void)
    27 {
    28         int err;
    29         err = request_irq(IRQ_EINT19, dummy_isr, IRQF_TRIGGER_FALLING | IRQF_SHARED, "dummy_isr", (void *)&dummy_cdev);
    30         if (err) {
    31                 printk(KERN_WARNING "request_irq IRQ_EINT19 for dummy_isr failed, error number is %d\n", err);
    32         } else {
    33                 printk(KERN_NOTICE "register dummy_isr succeed\n");
    34         }
    35   return 0;
    36 }
    37 static void __exit my_fini(void)
    38 {
    39         free_irq(IRQ_EINT19, (void *)&dummy_cdev);
    40         printk(KERN_NOTICE "unregister dummy_isr succeed\n");
    41 }

    s3c24xx_buttons_v2.5.c如下:

    63                 {IRQ_EINT19, IRQF_TRIGGER_FALLING | IRQF_SHARED, "KEY1"}, /* K1 */

    175         for (i = 0; i < BUTTON_NUM; i++) {
    177                 err = request_irq(buttons_dev.button_irqs[i].irqno, buttons_interrupt,
    178                                     buttons_dev.button_irqs[i].flags, buttons_dev.button_irqs[i].name,
    179                                     (void *)&buttons_dev);

    s3c24xx_buttons_v2.5.c与dummyisr.c注册了共享中断IRQ_EINT19,且均遵循了二中的1、2两点。(请务必注意在dummyisr中request_irq时除了要使用IRQF_SHARED标志外,还要使用IRQF_TRIGGER_FALLING标志,因为在button驱动中采用了该标志,二者必须一致,否则在注册第2个中断时总是不能成功,会出现如下错误。

    # insmod s3c24xx_buttons.ko
    buttons initialized
    # ./button_test &
    # open success
    输入回车
    # insmod dummyisr.ko
    request_irq IRQ_EINT19 for dummy_isr failed, error number is -16

    单击,与作者交流

    发表在 技术博客 | 留下评论

    中断顶半部与底半部

    单击,返回主页,查看更多内容

    一、区分和使用中断顶半部与底半部的原因

    • One of the main problems with interrupt handling is how to perform lengthy tasks within a handler. Often a substantial amount of work must be done in response to a device interrupt, but interrupt handlers need to finish up quickly and not keep interrupts blocked for long. These two needs (work and speed) conflict with each other, leaving the driver writer in a bit of a bind.
    • Linux (along with many other systems) resolves this problem by splitting the interrupt handler into two halves. The so-called top half is the routine that actually responds to the interrupt—the one you register with request_irq. The bottom half is a routine that is scheduled by the top half to be executed later, at a safer time. The big difference between the top-half handler and the bottom half is that all interrupts are enabled during execution of the bottom half—that’s why it runs at a safer time. In the typical scenario, the top half saves device data to a device-specific buffer, schedules its bottom half, and exits: this operation is very fast. The bottom half then performs whatever other work is required, such as awakening processes, starting up another I/O operation, and so on. This setup permits the top half to service a new interrupt while the bottom half is still working.
    • The Linux kernel has two different mechanisms that may be used to implement bottom-half processing. Tasklets are often the preferred mechanism for bottom-half processing; they are very fast, but all tasklet code must be atomic. The alternative to tasklets is workqueues, which may have a higher latency but that are allowed to sleep.

    二、tasklet机制

    1、原理

    • 类似内核定时器 ,调度一个任务稍后执行。主要用于中断的底半部
    • Tasklet是一个数据结构, 它指导内核在稍后CPU空闲的时间(但不能指定确定时间),使用一个用户定义的参数,执行一个用户定义的函数
    • 由内核线程——软中断(ksoftirqd/0)调度执行。在tasklet内不能睡眠
    • 运行在调度它们的同一个 CPU 上

    2、tasklet使用的内核API

    • include <linux/interrupt.h>
    • struct tasklet_struct {
          void (*func)(unsigned long);
          unsigned long data;
          其它字段
      };
    • 完全初始化tasklet
      • void tasklet_init(struct tasklet_struct *t, void (*func)(ulong), ulong data);
      • DECLARE_TASKLET(name, func, data);
      • DECLARE_TASKLET_DISABLED(name, func, data);
    • void tasklet_schedule(struct tasklet_struct *t);
      • 将tasklet加入系统链表,即调度tasklet运行
      • tasklet执行后会自动退出系统链表,如需再次执行,需再次加入系统链表
    • void tasklet_hi_schedule(struct tasklet_struct *t);
      • 以高优先级调度tasklet
    • void tasklet_kill(struct tasklet_struct *t);
      • 将tasklet退出系统链表
    • void tasklet_disable(struct tasklet_struct *t);
    • void tasklet_enable(struct tasklet_struct *t);
      • tasklet在disable的状态下可以被tasklet_schedule,但该tasklet必须等到tasklet_enable后才能真正运行
      • tasklet_enable的次数必须与tasklet_disable的次数相等

    三、Workqueue机制

    1、原理

    • 类似Tasklet,提交一个work到OS的特定workqueue——events/0上,以便稍后执行。主要用于中断的底半部
    • work是一个数据结构, 它指导内核在稍后的时间(但不能指定确定时间)或至少延迟一个确定的时间后,执行一个用户定义的函数(不能指定参数)
    • 由内核线程——events/0调度执行
      • 内核线程——events/0不处于atomic context
    • 缺省运行在调度它们的同一个 CPU 上

    # ps
      PID  Uid        VSZ Stat Command
        1 root       3092 S   init
        2 root            SW< [kthreadd]
        3 root            SWN [ksoftirqd/0]   //N表示低优先级, <表示高优先级
        4 root            SW< [events/0]
        5 root            SW< [khelper]
       41 root            SW< [kblockd/0]
       42 root            SW< [ksuspend_usbd]
       45 root            SW< [khubd]
       47 root            SW< [kseriod]
       59 root            SW  [pdflush]
       60 root            SW  [pdflush]
       61 root            SW< [kswapd0]
       62 root            SW< [aio/0]
      177 root            SW< [mtdblockd]
      226 root            SWN [jffs2_gcd_mtd2]
      248 root       1952 S   /usr/sbin/vsftpd
      249 root       3096 S   -sh
      250 root       3092 S   /usr/sbin/telnetd
      253 www        3092 S   /usr/sbin/httpd -h /www -u www
      256 root       3096 R   ps

    2、Tasklet与Workqueue的区别

    Tasklet

    Workqueue

    处于atomic context,不能sleep

    不处于atomic context,可以sleep

    运行在调度它们的同一个 CPU 上

    默认运行在调度它们的同一个 CPU 上

    不能指定确定时间进行调度

    不能指定确定时间进行调度或指定至少延迟一个确定的时间后调度

    只能提交给ksoftirqd/0

    可以提交给events/0,也可以提交给自定义的workqueue

    tasklet函数带参数

    work函数不带参数

     

    3、Workqueue API

    • DECLARE_WORK(name, void (*function)(work_struct *))
      • 编译时完全初始化一个work_struct
    • INIT_WORK(struct work_struct *work, void (*function)(work_struct *))
      • 完全初始化一个work_struct
    • int schedule_work(struct work_struct *work)
      • 将work提交给默认的kevent
    • •nt schedule_delayed_work(struct work_struct *work, ulong delay)
      • 将work提交给默认的kevent,但延后delay后调度
    • int cancel_delayed_work(struct work_struct *work);
      • 取消提交的work
    • void flush_scheduled_work(void)
      • 将kevent中的work全部取消
    • struct workqueue_struct *create_workqueue(const char *name)
    • struct workqueue_struct *create_singlethread_workqueue(const char *name)
      • 创建自己的workqueue,并与每个CPU(或单个CPU)绑定
    • int queue_work(struct workqueue_struct *queue, struct work_struct *work)
    • int queue_delayed_work(struct workqueue_struct *queue, struct work_struct *work, ulong delay)
      • 将work提交给queue
    • void flush_workqueue(struct workqueue_struct *queue)
      • 将queue中的work全部取消
    • void destroy_workqueue(struct workqueue_struct *queue)
      • 删除自己创建的queue

    四、tasklet实例(s3c24xx_buttons_v3.5.c)

    1、实例功能描述

    按下K1(K2、K3、K4)键时,LED1(LED2、LED3、LED4)闪烁1次。由于LED灯闪烁这个操作比较耗时(需3秒),因此不能在中断顶半部中执行,所以由顶半部调度它在中断底半部(本实例在tasklet)中运行。

    2、主要实现代码分析与说明

    314     for (i = 0; i < BUTTON_NUM; i++) /* intialize tasklets */
    315        tasklet_init(&(buttons_dev.button_tasklets[i]), button_do_tasklet, i);

    模块初始化时,完成对tasklet的初始化

    150     tasklet_schedule(&(buttons_dev.button_tasklets[butno-1])); /* schedule tasklet */

    在中断处理程序(顶半部)中,调度tasklet(底半部)在稍后安全的时间(非顶半部时间)运行

    82 static void button_do_tasklet(unsigned long data)
    83 {
    84     PDEBUG("enter tasklet, current pid is %d, button number is %li\n", current->pid, data+1);
    85     switch (data) {
    86         case 0:
    87            ledon(0);
    88            mdelay(1000);
    89             ledoff(0);
    90             mdelay(1000);
    91             ledon(0);
    92             break;
    93         case 1:
    116     }
    117     PDEBUG("exit tasklet, current pid is %d, button number is %li\n", current->pid, data+1);
    118 }

    在tasklet中调用mdelay完成了毫秒级的延时,由于mdelay是忙等待,而不是睡眠,所以可以在tasklet(中断上下文)中使用。

    在tasklet中,调用ledon和ledoff完成亮灯和灭灯的操作。ledon和ledoff并非是在本程序中定义的,而是在其它驱动内核模块中定义的,所以在编译本模块时会有警告,告知这2个函数在本模块中尚未定义。

    dennis@dennis-desktop:/work/studydriver/buttons$ make
    make -C /work/system/linux-2.6.22.6/ M=/work/studydriver/buttons LDDINC=/work/studydriver/buttons/../include modules
    make[1]: Entering directory `/work/system/linux-2.6.22.6′
      CC [M]  /work/studydriver/buttons/s3c24xx_buttons.o
      Building modules, stage 2.
      MODPOST 1 modules
    WARNING: "ledoff" [/work/studydriver/buttons/s3c24xx_buttons.ko] undefined!
    WARNING: "ledon" [/work/studydriver/buttons/s3c24xx_buttons.ko] undefined!
      CC      /work/studydriver/buttons/s3c24xx_buttons.mod.o
      LD [M]  /work/studydriver/buttons/s3c24xx_buttons.ko
    make[1]: Leaving directory `/work/system/linux-2.6.22.6′

    也正是由于这个原因,必须在加载本模块之前,先加载ledon和ledoff被定义的模块

    # insmod s3c24xx_buttons.ko
    s3c24xx_buttons: Unknown symbol ledoff
    s3c24xx_buttons: Unknown symbol ledon
    insmod: cannot insert ‘s3c24xx_buttons.ko’: Unknown symbol in module (-1): No such file or directory
    # insmod leds
    leds initialized
    # insmod s3c24xx_buttons.ko
    buttons initialized

    而且在ledon和ledoff被定义的模块(ledsv2.c)中,还必须用内核定义的EXPORT_SYMBOL宏将这2个函数导出到内核全局符号表,以供别的模块使用。

    93 EXPORT_SYMBOL(ledon);
    100 EXPORT_SYMBOL(ledoff);

    由于这2个函数是被导出到内核全局符号表的,因此不能与内核全局符号表中的其它函数(内核API)重名,否则就造成了内核名字空间污染。

    3、运行结果分析

    1 # ./button_test
    2 open success
    3 buttons: who(63) enter interrupt
    4 buttons: quit interrupt
    5 buttons: who(63) enter interrupt
    6 buttons: enter tasklet, current pid is 0, button number is 1
    7 buttons: who(63) enter interrupt
    8 buttons: who(63) enter interrupt
    9 buttons: who(63) enter interrupt
    10 buttons: who(63) enter interrupt
    11 buttons: who(63) enter interrupt
    延迟时长约2秒
    12 buttons: exit tasklet, current pid is 0, button number is 1
    13 read buttons successfully, begin print the result:
    14 K1 has been pressed 1 times!

    • 第6行说明tasklet的确是在稍后安全的时间(即:在顶半部退出后)运行的
    • 第5行说明tasklet的运行时间的确不能精确确定(即:并不是顶半部退出后立即运行),但会在顶半部退出后(此时CPU空闲)非常快就运行
    • 第7-11行出现在第6行和第12行之间,说明底半部运行期间,的确不禁用中断(顶半部),因此tasklet运行的时间段的确比较安全(这期间可以服务其它中断)
    • 第13-14行验证了tasklet的确是运行在中断上下文中。虽然测试进程在第4行之前就已经被唤醒,但它却要等到tasklet执行完(需要约2秒的时间),到12行后才有机会运行。这是因为tasklet运行在中断上下文中,期间OS除了响应中断外,不会进行进程调度。

    五、workqueue实例(s3c24xx_buttons_v4.5.c)

    1、实例功能描述

    按下K1(K2、K3、K4)键时,LED1(LED2、LED3、LED4)闪烁1次。由于LED灯闪烁这个操作比较耗时(需3秒),因此不能在中断顶半部中执行,所以由顶半部调度它在中断底半部(本实例在workqueue)中运行。

    2、主要实现代码分析与说明

    397     INIT_WORK(&(buttons_dev.buttons_workqueue), button_do_workqueue);   /* initialize workqueue */

    模块初始化时,完成对workqueue的初始化

    229     buttons_dev.dkeynum = butno – 1;
    230     schedule_work(&(buttons_dev.buttons_workqueue));    /* schedule workqueue */

    在中断处理程序(顶半部)中,调度workqueue(底半部)在稍后安全的时间(非顶半部时间)运行

    88 static void button_do_workqueue(struct work_struct * wq)
    89 {
    90     PDEBUG("enter workqueue, current pid is %d, button number is %d\n", current->pid, buttons_dev.dkeynum + 1);
    101     switch (buttons_dev.dkeynum) {
    102         case 0:
    103             ledon(0);
    104             ssleep(3);
    105             ledoff(0);
    106             ssleep(3);
    107             ledon(0);
    108             break;
    123         case 3:
    124             ledon(3);
    125             mdelay(3000);
    126             ledoff(3);
    127             mdelay(3000);
    128             ledon(3);
    129             break;
    132     }
    133     PDEBUG("exit workqueue, current pid is %d, button number is %d\n", current->pid, buttons_dev.dkeynum + 1);
    134 }

    在workqueue与tasklet一个重大区别就是可以睡眠,所以它可以调用mdelay进行忙等待,也可以安全地调用ssleep睡眠。

    3、运行结果分析

    1 # ./button_test
    2 open success
    3 按下K1键
    4 buttons: who(63) enter interrupt, current pid is 0
    5 buttons: in interrupt, CPSR = 60000093, SPSR = 80000013
    6 buttons: who(63) quit interrupt, current pid is 0, button number is 1
    7 buttons: enter workqueue, current pid is 4, button number is 1
    8 buttons: in workqueue, CPSR = 60000013, SPSR = 60000013
    9 read buttons successfully, begin print the result:
    10 K1 has been pressed 1 times!
    11 buttons: who(63) enter interrupt, current pid is 0, button number is 0
    12 buttons: who(63) enter interrupt, current pid is 0, button number is 0
    13 buttons: who(63) enter interrupt, current pid is 0, button number is 0
    14 延迟时长约6秒
    15 buttons: exit workqueue, current pid is 4, button number is 1
    16 按下K4键
    17 buttons: who(16) enter interrupt, current pid is 0
    18 buttons: in interrupt, CPSR = 60000013, SPSR = 60000013
    19 buttons: who(16) quit interrupt, current pid is 0, button number is 4
    20 buttons: who(16) enter interrupt, current pid is 0
    21 buttons: who(16) enter interrupt, current pid is 0
    22 buttons: enter workqueue, current pid is 4, button number is 4
    23 buttons: in workqueue, CPSR = 60000013, SPSR = 60000013
    24 buttons: who(16) enter interrupt, current pid is 4
    25 buttons: who(16) enter interrupt, current pid is 4
    26 延迟时长约6秒
    27 buttons: exit workqueue, current pid is 4, button number is 4
    28 read buttons successfully, begin print the result:
    29 K4 has been pressed 1 times!

    • 第5行说明本次中断处理程序(顶半部)运行时,禁用了IRQ异常(请比对第18行)。这是注册中断时指定中断选项的结果。

        75     .button_irqs   = {
        76         {IRQ_EINT19, IRQF_TRIGGER_FALLING|IRQF_DISABLED, "KEY1"}, /* K1 */
        77         {IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /* K2 */
        78         {IRQ_EINT2,  IRQF_TRIGGER_FALLING, "KEY3"}, /* K3 */
        79         {IRQ_EINT0,  IRQF_TRIGGER_FALLING, "KEY4"}  /* K4 */
        80     },

        不过为何处于SVC模式而不是IRQ模式有待阅读内核源码,可能是内核处理中断时为了能够中断嵌套进行了模式切换

    • 第7行验证了由内核线程——events/0(它的pid是4)调度执行workqueue
    • 第9、10行出现在第7与15行之间,说明workqueue的确可以睡眠,从而使得操作系统可以调度其它用户进程运行
    • 第20、21行说明workqueue的运行时间的确不能精确确定(即:并不是顶半部退出后立即运行),但会在顶半部退出后(此时CPU空闲)非常快就运行
    • 第24-25行出现在第22行和第27行之间,说明底半部运行期间,的确不禁用中断(顶半部),因此workqueue运行的时间段的确比较安全(这期间可以服务其它中断)
    • 测试进程在19行之后就已经被唤醒,但第28-29出现在27行之后,说明了虽然workqueue运行在进程上下文中,但它是高优先级内核线程,所以只要它不主动让出CPU,操作系统也几乎不会(不是肯定不会)调度普通用户进程运行。这一点与tasklet不同,tasklet运行在中断上下文中,期间OS除了响应中断外,肯定不会进行进程调度。所以得出结论:
      • 必须立即进行紧急处理的极少量任务放在顶半部中。此时屏蔽了与自己同类型的中断,由于任务量极少,所以可以迅速不受打扰地处理完紧急任务;
      • 需要较少时间处理的中等数量的急迫任务放在tasklet中。此时不会屏蔽任何中断(包括与自己的顶半部同类型的中断),所以不影响顶半部对紧急任务的处理;同时又不会进行用户进程调度,从而保证了自己的急迫任务得以迅速完成;
      • 需要较多时间处理且并不急迫(允许被操作系统剥夺运行权)的大量任务放在workqueue中。此时操作系统会尽量快速处理完这个任务,但如果任务量太大,期间操作系统也有机会调度别的用户进程运行,从而保证了不会因为这个任务需要运行的时间太长而将其它用户进程饿死。
      • 可能引起睡眠的任务放在workqueue中。因为在workqueue中睡眠是安全的

    4、验证在tasklet中不能睡眠

    将第54行注释掉,从而使用tasklet。

    54 //#define USEWORKQUEUE

    136 static void button_do_tasklet(unsigned long data)
    137 {
    149         switch (data) {
    150                 case 0:
    151                         ledon(0);
    152                         mdelay(1000);
    153                         ledoff(0);
    154                         mdelay(1000);
    155                         ledon(0);
    156                         break;
    171                 case 3:
    172                         ledon(3);
    173                         mdelay(1000);
    174                         ledoff(3);
    175                         ssleep(1);
    176                         //mdelay(1000);
    177                         ledon(3);
    178                         break;
    181         }
    183 }

    运行的结果如下,验证了在tasklet的确不能睡眠(有时间的话,就好好看看OOP的输出吧,也许你能发现一些内核的小秘密!):

    # ./button_test
    open success
    按下K1键
    buttons: who(63) enter interrupt, current pid is 0
    buttons: in interrupt, CPSR = 60000093, SPSR = 93
    buttons: who(63) quit interrupt, current pid is 0, button number is 1
    buttons: who(63) enter interrupt, current pid is 0
    buttons: enter tasklet, current pid is 0, button number is 1
    buttons: in tasklet, CPSR = 60000013, SPSR = 60000013
    buttons: who(63) enter interrupt, current pid is 0
    buttons: who(63) enter interrupt, current pid is 0
    buttons: who(63) enter interrupt, current pid is 0
    buttons: who(63) enter interrupt, current pid is 0
    buttons: who(63) enter interrupt, current pid is 0
    buttons: exit tasklet, current pid is 0, button number is 1
    read buttons successfully, begin print the result:
    K1 has been pressed 1 times!
    按下K4键
    buttons: who(16) enter interrupt, current pid is 0
    buttons: in interrupt, CPSR = 60000013, SPSR = 80000013
    buttons: who(16) quit interrupt, current pid is 0, button number is 4
    buttons: enter tasklet, current pid is 0, button number is 4
    buttons: in tasklet, CPSR = 60000013, SPSR = 60000013
    BUG: scheduling while atomic: swapper/0x00000100/0
    [<c0028c90>] (dump_stack+0x0/0x14) from [<c0200cf0>] (schedule+0x50/0x750)
    [<c0200ca0>] (schedule+0x0/0x750) from [<c0201dec>] (schedule_timeout+0x8c/0xbc)
    [<c0201d60>] (schedule_timeout+0x0/0xbc) from [<c0201e68>] (schedule_timeout_uninterruptible+0x24/0x28)
    r8:3001f48c r7:c02ce280 r6:00000004 r5:c0282000 r4:ffffffff
    [<c0201e44>] (schedule_timeout_uninterruptible+0x0/0x28) from [<c00442f4>] (msleep+0x1c/0x28)
    [<c00442d8>] (msleep+0x0/0x28) from [<bf00208c>] (button_do_tasklet+0x8c/0x1c8 [s3c24xx_buttons])
    [<bf002000>] (button_do_tasklet+0x0/0x1c8 [s3c24xx_buttons]) from [<c00404c0>] (tasklet_action+0x88/0xdc)
    r6:0000000a r5:c02ce2a4 r4:00000000
    [<c0040438>] (tasklet_action+0x0/0xdc) from [<c0040000>] (__do_softirq+0x5c/0xc8)
    r5:c02ce2e4 r4:00000001
    [<c003ffa4>] (__do_softirq+0x0/0xc8) from [<c0040224>] (irq_exit+0x44/0x4c)
    r7:00000000 r6:c02d2edc r5:c028b0a8 r4:00000010
    [<c00401e0>] (irq_exit+0x0/0x4c) from [<c002404c>] (asm_do_IRQ+0x4c/0x60)
    [<c0024000>] (asm_do_IRQ+0x0/0x60) from [<c0024a24>] (__irq_svc+0x24/0xa0)
    Exception stack(0xc0283f54 to 0xc0283f9c)
    3f40:                                              00000000 ffffffff f020000c
    3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
    3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff         
    r7:c02dfb58 r6:00000001 r5:f0000000 r4:ffffffff
    [<c0025974>] (default_idle+0x0/0x78) from [<c0025a34>] (cpu_idle+0x48/0x64)
    [<c00259ec>] (cpu_idle+0x0/0x64) from [<c02006c4>] (rest_init+0x48/0x58)
    r5:c02bc328 r4:c02d12d4
    [<c020067c>] (rest_init+0x0/0x58) from [<c0008938>] (start_kernel+0x27c/0x2e4)
    [<c00086bc>] (start_kernel+0x0/0x2e4) from [<30008030>] (0x30008030)
    bad: scheduling from the idle thread!
    [<c0028c90>] (dump_stack+0x0/0x14) from [<c0200d3c>] (schedule+0x9c/0x750)
    [<c0200ca0>] (schedule+0x0/0x750) from [<c0201dec>] (schedule_timeout+0x8c/0xbc)
    [<c0201d60>] (schedule_timeout+0x0/0xbc) from [<c0201e68>] (schedule_timeout_uninterruptible+0x24/0x28)
    r8:3001f48c r7:c02ce280 r6:00000004 r5:c0282000 r4:ffffffff
    [<c0201e44>] (schedule_timeout_uninterruptible+0x0/0x28) from [<c00442f4>] (msleep+0x1c/0x28)
    [<c00442d8>] (msleep+0x0/0x28) from [<bf00208c>] (button_do_tasklet+0x8c/0x1c8 [s3c24xx_buttons])
    [<bf002000>] (button_do_tasklet+0x0/0x1c8 [s3c24xx_buttons]) from [<c00404c0>] (tasklet_action+0x88/0xdc)
    r6:0000000a r5:c02ce2a4 r4:00000000
    [<c0040438>] (tasklet_action+0x0/0xdc) from [<c0040000>] (__do_softirq+0x5c/0xc8)
    r5:c02ce2e4 r4:00000001
    [<c003ffa4>] (__do_softirq+0x0/0xc8) from [<c0040224>] (irq_exit+0x44/0x4c)
    r7:00000000 r6:c02d2edc r5:c028b0a8 r4:00000010
    [<c00401e0>] (irq_exit+0x0/0x4c) from [<c002404c>] (asm_do_IRQ+0x4c/0x60)
    [<c0024000>] (asm_do_IRQ+0x0/0x60) from [<c0024a24>] (__irq_svc+0x24/0xa0)
    Exception stack(0xc0283f54 to 0xc0283f9c)
    3f40:                                              00000000 ffffffff f020000c
    3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
    3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff         
    r7:c02dfb58 r6:00000001 r5:f0000000 r4:ffffffff
    [<c0025974>] (default_idle+0x0/0x78) from [<c0025a34>] (cpu_idle+0x48/0x64)
    [<c00259ec>] (cpu_idle+0x0/0x64) from [<c02006c4>] (rest_init+0x48/0x58)
    r5:c02bc328 r4:c02d12d4
    [<c020067c>] (rest_init+0x0/0x58) from [<c0008938>] (start_kernel+0x27c/0x2e4)
    [<c00086bc>] (start_kernel+0x0/0x2e4) from [<30008030>] (0x30008030)
    Unable to handle kernel NULL pointer dereference at virtual address 00000000
    pgd = c0004000
    [00000000] *pgd=00000000
    Internal error: Oops: 17 [#1]
    Modules linked in: s3c24xx_buttons leds
    CPU: 0    Not tainted  (2.6.22.6 #22)
    PC is at dequeue_task+0xc/0x84
    LR is at deactivate_task+0x34/0x40
    pc : [<c0036b44>]    lr : [<c0036ec8>]    psr: 60000093
    sp : c0283e14  ip : c0283e28  fp : c0283e24
    r10: 004c4b18  r9 : 894f8c40  r8 : c0284ea0
    r7 : c0283e74  r6 : 000000c9  r5 : c0282000  r4 : c0284ea0
    r3 : 00000080  r2 : 00000080  r1 : 00000000  r0 : c0284ea0
    Flags: nZCv  IRQs off  FIQs on  Mode SVC_32  Segment kernel
    Control: c000717f  Table: 338c8000  DAC: 00000017
    Process swapper (pid: 0, stack limit = 0xc0282258)
    Stack: (0xc0283e14 to 0xc0284000)
    3e00:                                              c0284ea0 c0283e38 c0283e28
    3e20: c0036ec8 c0036b48 004c4b18 c0283e70 c0283e3c c0200e50 c0036ea4 c0284edc
    3e40: c0284fac 3b9aca00 ffff7c22 c0282000 000000c9 c0283e74 c028a4d0 41129200
    3e60: 3001f458 c0283eac c0283e74 c0201dec c0200cb0 c02cee88 c02cee88 ffff7c22
    3e80: c00440a8 c0284ea0 c02ce4a0 ffffffff c0282000 00000004 c02ce280 3001f48c
    3ea0: c0283ebc c0283eb0 c0201e68 c0201d70 c0283ecc c0283ec0 c00442f4 c0201e54
    3ec0: c0283ee8 c0283ed0 bf00208c c00442e8 00000000 c02ce2a4 0000000a c0283f00
    3ee0: c0283eec c00404c0 bf002010 00000001 c02ce2e4 c0283f20 c0283f04 c0040000
    3f00: c0040448 00000010 c028b0a8 c02d2edc 00000000 c0283f30 c0283f24 c0040224
    3f20: c003ffb4 c0283f50 c0283f34 c002404c c00401f0 ffffffff f0000000 00000001
    3f40: c02dfb58 c0283fa8 c0283f54 c0024a24 c0024010 00000000 ffffffff f020000c
    3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
    3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff c0283fc0
    3fa0: c0283fac c0025a34 c0025984 c02d12d4 c02bc328 c0283fd0 c0283fc4 c02006c4
    3fc0: c00259fc c0283ff4 c0283fd4 c0008938 c020068c c0008324 c0020f28 c0007175
    3fe0: c02bc7e4 c0285c9c 00000000 c0283ff8 30008030 c00086cc 00000000 00000000
    Backtrace:
    [<c0036b38>] (dequeue_task+0x0/0x84) from [<c0036ec8>] (deactivate_task+0x34/0x40)
    r4:c0284ea0
    [<c0036e94>] (deactivate_task+0x0/0x40) from [<c0200e50>] (schedule+0x1b0/0x750)
    r4:004c4b18
    [<c0200ca0>] (schedule+0x0/0x750) from [<c0201dec>] (schedule_timeout+0x8c/0xbc)
    [<c0201d60>] (schedule_timeout+0x0/0xbc) from [<c0201e68>] (schedule_timeout_uninterruptible+0x24/0x28)
    r8:3001f48c r7:c02ce280 r6:00000004 r5:c0282000 r4:ffffffff
    [<c0201e44>] (schedule_timeout_uninterruptible+0x0/0x28) from [<c00442f4>] (msleep+0x1c/0x28)
    [<c00442d8>] (msleep+0x0/0x28) from [<bf00208c>] (button_do_tasklet+0x8c/0x1c8 [s3c24xx_buttons])
    [<bf002000>] (button_do_tasklet+0x0/0x1c8 [s3c24xx_buttons]) from [<c00404c0>] (tasklet_action+0x88/0xdc)
    r6:0000000a r5:c02ce2a4 r4:00000000
    [<c0040438>] (tasklet_action+0x0/0xdc) from [<c0040000>] (__do_softirq+0x5c/0xc8)
    r5:c02ce2e4 r4:00000001
    [<c003ffa4>] (__do_softirq+0x0/0xc8) from [<c0040224>] (irq_exit+0x44/0x4c)
    r7:00000000 r6:c02d2edc r5:c028b0a8 r4:00000010
    [<c00401e0>] (irq_exit+0x0/0x4c) from [<c002404c>] (asm_do_IRQ+0x4c/0x60)
    [<c0024000>] (asm_do_IRQ+0x0/0x60) from [<c0024a24>] (__irq_svc+0x24/0xa0)
    Exception stack(0xc0283f54 to 0xc0283f9c)
    3f40:                                              00000000 ffffffff f020000c
    3f60: 80000013 c0025974 c0282000 c0020f28 c02dfb58 3001f48c 41129200 3001f458
    3f80: c0283fa8 c0283f9c c0283f9c c00259d4 c00259e0 80000013 ffffffff         
    r7:c02dfb58 r6:00000001 r5:f0000000 r4:ffffffff
    [<c0025974>] (default_idle+0x0/0x78) from [<c0025a34>] (cpu_idle+0x48/0x64)
    [<c00259ec>] (cpu_idle+0x0/0x64) from [<c02006c4>] (rest_init+0x48/0x58)
    r5:c02bc328 r4:c02d12d4
    [<c020067c>] (rest_init+0x0/0x58) from [<c0008938>] (start_kernel+0x27c/0x2e4)
    [<c00086bc>] (start_kernel+0x0/0x2e4) from [<30008030>] (0x30008030)
    Code: c02bcffc e1a0c00d e92dd810 e24cb004 (e5913000)
    Kernel panic – not syncing: Fatal exception in interrupt

    单击,与作者交流

    发表在 技术博客 | 留下评论

    内核时间与内核定时器

    单击,返回主页,查看更多内容

    一、内核中如何记录时间

    任何程序都需要时间控制,其主要目的是:

    • 测量时间流逝和比较时间
    • 知道当前时间
    • 指定时间量的延时操作

    为达到这个目的,应用程序使用日历时间(年月日时分秒)或者自1970年1月1日零时零分零秒到当前的秒数来度量时间的流逝,但内核中需要更加有精度的时间度量,因此内核使用时钟嘀嗒来记录时间。时钟中断发生后内核内部时间计数器增加1(即:增加1个时钟嘀嗒),系统引导时为0,当前值为自上次系统引导以来的时钟滴答数,程序可通过内核定义的全局变量jiffies_64或jiffies来访问。真实硬件上每秒的嘀嗒数从 50 到 1200 不等 ,x86上默认为1000,而s3c2440上默认为200,出于统一编程接口的考虑,内核定义了一个宏HZ,它表示1秒钟的嘀嗒数,可供程序使用。

    • Jiffies 和 jiffies_64是 unsigned long 类型只读变量,其用法如下:
      • #include <linux/jiffies.h>
      • unsigned long j, stamp_1, stamp_half, stamp_n;
      • j = jiffies; /* read the current value */
      • stamp_1 = j + HZ; /* 1 second in the future */
      • stamp_half = j + HZ/2; /* half a second in the future*/
      • stamp_n = j + n * HZ / 1000; /* n milliseconds in the future */

    注意:32-位 平台上当 HZ 是 1000 时, 计数器只是每 50 天溢出一次, 必要时你的代码应当准备处理这个事件

    • 比较2个时间的大小,常用内核的如下宏定义:
      • #include <linux/jiffies.h>
      • int time_after(unsigned long a, unsigned long b);
      • int time_before(unsigned long a, unsigned long b);
      • int time_after_eq(unsigned long a, unsigned long b);
      • int time_before_eq(unsigned long a, unsigned long b);

    它们分别在时间a在时间b之后、之前、之后或相等、之前或相等的时候为真,反之为假

    • 求 2 个 jiffies 实例之间的差:
      • diff = (long)t2 – (long)t1;.
    • 你可以转换一个 jiffies 差为毫秒, 一般地通过:
      • msec = diff * 1000 / HZ;
    • jiffies与日历时间的转换函数:
      • #include <linux/time.h>
      • unsigned long timespec_to_jiffies(struct timespec *value);
      • void jiffies_to_timespec(unsigned long jiffies, struct timespec *value);
      • unsigned long timeval_to_jiffies(struct timeval *value);
      • void jiffies_to_timeval(unsigned long jiffies, struct *timeval)

    二、内核定时器

    1、概述

    • 无论何时你需要调度一个动作以后发生, 就可以使用内核定时器,如:当硬件无法发出中断时, 可通过使用内核定时器,以定期的间隔检查一个设备的状态。
    • 一个内核定时器是一个数据结构, 它指导内核在一个用户定义的时间,使用一个用户定义的参数,执行一个用户定义的函数
    • 由内核线程——软中断(ksoftirqd/0)调度执行
      • 1个CPU,1个ksoftirqd
      • ksoftirqd属于atomic context
      • ksoftirqd运行时,不禁用irq

    2、定时器 API

    • #include <linux/timer.h>
    • struct timer_list {
          unsigned long expires;
          void (*function)(unsigned long);
          unsigned long data;
          其它字段
      };
    • 静态初始化定时器结构体
      • struct timer_list timerval = TIMER_INITIALIZER(_function, _expires, _data);
    • 动态初始化定时器结构体
      • setup_timer (struct timer_list *timer, _function, _data);   //初始化function和data后,调用init_timer
      • void init_timer(struct timer_list *timer); //初始化其它字段
      • timer->expires = jiffies + HZ/10;  //最后手工指定触发时间
    • 将已初始化定时器加入系统定时器链表
      • void add_timer(struct timer_list * timer);
      • 注:定时器执行后会自动退出系统链表,如需再次执行,需要更新expires后,再次加入系统链表
    • 更新一个定时器的超时时间,同时加入系统链表
      • int mod_timer(struct timer_list *timer, unsigned long expires);
    • 将已链入但还未执行的定时器退出系统链表
      • int del_timer(struct timer_list * timer);

    三、内核定时器与内核时间的应用案例——按键消抖

    在“驱动程序中的中断处理”一文的实验中,如果将编译s3c2440_buttons_v2.5.c改为编译s3c2440_buttons_v1.c(rm s3c2440_buttons.c; ln –s s3c2440_buttons_v1.c s3c2440_buttons.c; make),你可能注意到了,当仅按1次按键的时候,结果显示有多次按键,这跟我们理想的状态有些差异,出现这种情况是由于存在按键抖动。如下的显示,是由于一次人工按键共产生了4次中断:第1次中断唤醒了测试进程,并且测试进程在第2次中断产生前成功读取了按键次数;第2次中断唤醒了测试进程,但在测试进程尚未成功读取按键次数前,第3次中断迅速产生,因此当测试进程赶在第4次中断产生前读取按键次数时,该值已经变为了2;第4次中断唤醒了测试进程,同时由于按键已经平稳,不再产生新的中断,从而测试进程成功读取了按键次数。

    # ./button_test
    open success
    read buttons successfully, begin print the result:
    K1 has been pressed 1 times!
    read buttons successfully, begin print the result:
    K1 has been pressed 2 times!
    read buttons successfully, begin print the result:
    K1 has been pressed 1 times!

    按键的物理特性决定它肯定会存在抖动。因此要消除抖动,只能采用软件消抖的方法。请编译s3c24xx_buttons_v2.5.c,可得到消除了按键抖动的驱动版本。

    # ./button_test
    open success
    read buttons successfully, begin print the result:
    K1 has been pressed 1 times!

    s3c24xx_buttons_v2.5.c是怎么做到的呢?其消抖方案是:只在第1次中断产生时才记录按键次数;在第1次中断产生后,多次延时0.1秒直到检测到按键已被放开,最后再做1次延迟0.1秒的操作,然后允许中断可以再次记录按键次数。这样一来,第1次延迟0.1秒消除了按下键时的抖动,最后1次延迟0.1秒消除了放开键时的抖动。这个方案用到的技术主要就是内核定时器和内核时间

     

    下面对主要实现代码进行说明:

    58 static struct buttons_dev_t buttons_dev =
    59 {
    60         .ev_press   = 0,
    61         .press_cnt    = {0, 0, 0, 0},
    62         .button_irqs   = {
    63                 {IRQ_EINT19, IRQF_TRIGGER_FALLING, "KEY1"}, /* K1 */
    64                 {IRQ_EINT11, IRQF_TRIGGER_FALLING, "KEY2"}, /* K2 */
    65                 {IRQ_EINT2,  IRQF_TRIGGER_FALLING, "KEY3"}, /* K3 */
    66                 {IRQ_EINT0,  IRQF_TRIGGER_FALLING, "KEY4"}  /* K4 */
    67         },
    68         .firstint   =  0
    69 };

    79         if (buttons_dev.firstint == 1)
    80                 return IRQ_RETVAL(IRQ_NONE);
    82         buttons_dev.firstint = 1;

    中断处理函数如果发现不是第1次中断,就不记录按键次数。

    277         for (i = 0; i < BUTTON_NUM; i++) /* setup button delay timers without set expires. default 4 timers */
    278                 setup_timer(&(buttons_dev.button_timers[i]), buttons_timer_handler, i);

    这是在模块的初始化函数中对4个定时器(分别对应4个按键)进行初始化。

    105         buttons_dev.button_timers[butno-1].expires = jiffies + HZ/10; /* delay 0.1s */
    106         add_timer(&(buttons_dev.button_timers[butno-1]));

    中断处理函数做第1次0.1秒的延时。

    152 static void buttons_timer_handler(unsigned long data)
    153 {
    154         static int shouldfinish = 0;
    157         if (shouldfinish) {
    158                 shouldfinish  = 0;
    159                 buttons_dev.firstint = 0;
    160                 return;
    161         }
    162         if (!keydown(data))
    163                 shouldfinish = 1;
    164         mod_timer(&(buttons_dev.button_timers[data]), jiffies + HZ/10); /* delay 0.1s */
    165 }

    定时器处理函数进行多次0.1秒的延时(164行),并在判定出按键已被放开的情况下作最后一次0.1秒延迟(162-164行),之后允许中断处理函数可以再次记录按键次数(159行)。

    41 #define GPGDAT           0x56000064

    296         if (!(button12virtaddr = ioremap(GPGDAT, 0x4))) {
    297                 printk(KERN_NOTICE "ioremap failed\n");
    298                 result = -ENOMEM;
    299                 goto fail_ioremap_GPGDAT;
    300         }

    112 static int keydown(unsigned long data)
    113 {
    114         int result;
    115         switch (data) {
    116                 case 0:
    117                         PDEBUGG("K1\n");
    118                        if ((ioread32(button12virtaddr) & (1<<11)) == 0)
    119                                 result = 1;
    120                         else
    121                                 result = 0;
    122                         break;
    123                 case 1:
    147         }
    149         return result;
    150 }

    keydown函数用于判定某个按键是否按下。按下返回真,放开返回假。关于获得按键是否被按下的机制,请参阅“ARM体系结构与编程”中的相关文章。

    三、如何在内核中实现延时

    设备驱动常常需要延后一段时间执行一个特定片段的代码, 以便允许硬件完成某个任务。延时一般区分为短延时和长延时。

    1、短延时

    当一个设备驱动需要等待硬件的反应时间, 涉及到的延时常常是最多几个毫秒 。此种延时就是短延时,一般采用忙等待。相关函数如下:

    • #include <linux/delay.h>
    • void ndelay(unsigned long nsecs);
    • void udelay(unsigned long usecs);
    • void mdelay(unsigned long msecs);

    2、长延时

    如果需要延后较长时间,就可以采用长延时。长延时可分为忙等待和让出CPU两种方式。

    1)、忙等待

    unsigned long j1 = jiffies + 2*HZ;
    while (time_before(jiffies, j1))
        cpu_relax();

    cpu_relex 的调用使用了一个特定于体系的方式,你此时没有用处理器做事情。

    2)、让出处理器

    unsigned long j1 = jiffies + 3600*HZ;
    while (time_before(jiffies, j))
        schedule();

    忙等待强加了一个重负载给系统总体,通过释放CPU改变这种状况。

    3)、此外,如果你的驱动使用一个等待队列来等待某些其他事件,但是你也想确保它在一个确定时间段内运行能够运行,而不是永久等待,那么可以使用超时

    • #include <linux/wait.h>
    • long wait_event_timeout(wait_queue_head_t q, condition, long timeout);
    • long wait_event_interruptible_timeout(wait_queue_head_t q, condition, long timeout);

     

     

    单击,与作者交流

    发表在 技术博客 | 留下评论