Linux设备驱动实例

时间:2022-12-18 19:03:52

13 Linux块设备驱动

        块设备是字符设备并列的概念,这两类设备在Linux中驱动的结构有较大差异,总体而言,块设备驱动比字符设备驱动要复杂得多,在I/O操作上表现出极大的不同,缓冲、I/O调度、请求队列等都是与块设备驱动相关的概念。

        13.1 块设备的I/O操作特点

                字符设备与块设备I/O操作的不同如下:

                        (1)块设备只能以块为单位接受输入和返回输出,而字符设备则以字节为单位,大多数设备是字符设备,因为它们不需要缓冲而且不以固定块大小进行操作。

                        (2)块设备对于I/O请求有相对应的缓冲区,因此它们可以选择以什么顺序进行响应,字符设备无须缓冲且被直接读写。对于存储设备而言调整读写的顺序作用巨大,因为在读写连续的扇区比分离的扇区更快。

                        (3)字符设备只能被顺序读写,而块设备可以随机访问,虽然块设备可随机访问,但是对于磁盘这类机械设备而言,顺序地组织块设备的访问可以提高性能。如下图所示,对扇区1、10、3、2的请求被调整为对扇区1、2、3、10的请求。而对SD卡、RamDisk等块设备而言,不存在机械上的原因,进行这样的调整没有必要。

                                 Linux设备驱动实例

        13.2 Linux块设备驱动结构

                13.2.1 block_device_operations结构体

                        在块设备驱动中,有一个类似于字符设备驱动中file_operations结构体的block_device_operations结构体,它是对块设备操作的集合。定义代码如下所示:

                                struct block_device_operations{

                                        int(*open)(struct block_device *, fmode_t);

                                        int(*release)(struct gendisk *, fmode_t);

                                        int(*locked_ioctl)(struct block_device *, fmode_t, unsigned, unsigned long);

                                        int(*ioctl)(struct block_device*, fmode_t, unsigned, unsigned long);

                                        int(*compat_ioctl)(struct block_device*, fmode_t, unsigned, unsigned long);

                                        int(*direct_access)(struct block_device*, sector_t, void**, unsigned long *);

                                        int(*media_changed)(struct gendisk*);

                                        int(*revalidata_disk)(struct gendisk*);

                                        int(*getgeo)(struct block_device*, struct hd_geometry *);

                                        struct module *owner;

                                };

                        1.打开和释放

                                 int(*open)(struct gendisk *disk, fmode_t mode);

                                 int(*release)(struct gendisk *disk, fmode_t mode);

                                 与字符设备驱动类似,当设备被打开和关闭时将调用它们。

                         2.I/O控制

                                 int(*ioctl)(struct block_device *bdev, fmode_t mode, unsigned cmd, unsigned long arg);

                                 上述函数是系统调用的实现,块设备包含大量的标准请求,这些标准请求由Linux块设备层处理,因此大部分块设备驱动的ioctl()函数相当短。

                         3.介质改变

                                 int (*media_changed)(struct gendisk *gd);

                                 被内核调用来检查是否驱动器中的介质已经改变,如果是,则返回一个非0值,否则返回0.这个函数仅适合用于支持可移动介质的驱动器,通常需要在驱动中增加一个表示介质状态是否改变的标志变量,非可移动设备的驱动不需要实现这个方法。

                        4.使介质有效

                                int (*revalidata_disk)(struct gendisk *gd);

                                revalidata_disk()函数被调用来响应一个介质改变,它给驱动一个机会来进行必要的工作以使新介质准备好。

                        5.获得驱动器信息

                                int (*getgeo)(struct block_device *, struct hd_geomotry *);

                                该函数根据驱动器的几何信息填充一个hd_geometry结构体,hd_geometry结构体包含磁头、扇区、柱面等信息,其定义于include/linux/hdreg.h头文件。

                        6.模块指针

                                struct module *owner;

                                一个指向拥有这个结构体的模块指针,它通常被初始化为THIS_MODULE。

                13.2.2 gendisk结构体

                         在Linux内核中,使用gendisk(通用磁盘)结构体来表示一个独立的磁盘设备(或分区),这个结构体的定义如下:

                                 struct gendisk{

                                         int major;        /*主设备号*/

                                         int first_minor; 

                                         int minors;        /*最大的次设备数,如果不能分区,则为1*/

                                         char disk_name[DISK_NSME_LEN];        /*设备名称*/

                                         struct disk_part_tb1 *part_tb1;        /*有partno索引的分区指针的数组*/

                                         struct hd_struct part0;

                                         struct block_device_operations *fops;        /*块设备操作集合*/

                                         struct request_queue *queue;

                                         void *private_data;

                                         int flags;

                                         struct device *driverfs_dev;        /*FIXME:remove*/

                                         struct kobject *slave_dir;

                                         struct timer_rand_state *random;

                                         atomic_t sync_io;        /*RAID*/

                                         struct work_struct async_notify;

                                 #ifdef    CONFIG_BLK_DEV_INTEGRITY

                                         struct blk_integrity *integrity;

                                 #endif

                                         int mode_id;

                                 };

                         major、first_minor和minors共同表征了磁盘的主、次设备号,同一个磁盘的各个分区共享一个主设备号,而次设备号则不同。fops为block_device_operations,即上节描述的块设备操作集合。queue是内核用来管理这个设备的I/O请求队列的指针。private_data可用于指向磁盘的任何私有数据,用法与字符设备驱动file结构体的private_data类似。hd_struct成员表示一个分区,而disk_part_tb1成员容纳分区表,part()和part_tb1二者的关系在于:disk->part_tb1->part[0]=&disk->part0;

                         Linux内核提供了一组函数操作gendisk,如下所示。

                                 1.分配gendisk

                                          gendisk结构体是一个动态分配的结构体,它需要特别的内核操作来初始化,驱动不能自己分配这个结构体,而应用使用下列函数来分配gendisk:

                                                  struct gendisl *alloc_disk(int minors);

                                          minors参数是这个磁盘使用的次设备号的数量,一般也就是磁盘分区的数量,此后minors不能被修改。

                                  2.增加gendisk

                                          gendisk结构体被分配之后,系统还不能使用这个磁盘,需要调用如下函数来注册这个磁盘设备。

                                                  void add_disk(struct gendisk *data);

                                          特别要注意的是对add_disk()的调用必须发生在驱动程序的初始化工作完成并能相应磁盘的请求之后。

                                  3.释放gendisk

                                          当不再需要一个磁盘时,应当使用如下函数释放gendisk。

                                                  void del_gendisk(struct gendisk *gp);

                                   4.gendisk引用计数

                                           通过get_disk()和put_disk()函数可用来操作gendisk的引用计数,这个工作一般不需要驱动亲自做,这两个函数的原型分别为:

                                                   struct kobject *get_disk(struct gendisk *disk);

                                                   void put_disk(struct gendisk *disk);

                                           前者最终会调用kobject_get(&disk_to_dev(disk)->kobj);而后者则会调用kobject_put(&disk_to_dev(disk)->kobj);

                13.2.3 request与bio结构体

                        1.请求

                                 在Linux块设备驱动中,使用request结构体来表征等待进行的I/O请求。

                                 request结构体的主要成员包括:

                                         sector_t hard_sctor;        /*是第一个尚未传输的扇区*/    以下三个成员标识尚未完成的扇区

                                         unsigned long hard_nr_sectors;        /*是尚待完成的扇区数*/

                                         unsigned int hard_cur_sectors;        /*是当前I/O操作中待完成的扇区数*/

                                         sector_t sector;

                                         unsigned long nr_sectors;

                                         unsigned int current_nr_sectors;

                                         驱动中会经常与这3个成员打交道,这3个成员在内核和驱动交互中发挥着重大作用。他们以512字节大小为一个扇区,如果硬件的扇区大小不是512字节,则需要进行相应的调整。例如,如果硬件的扇区大小是2048字节,则在进行硬件操作之前,需要用4来除起始扇区号。

                                         hard_sector、hard_nr_sectors、hard_cur_sectors与sector、nr_sectors、current_nr_sectors之间可认为是“副本”关系。

                                         struct bio *bio:bio是这个请求中包含的bio结构体的链表,驱动中不宜直接存取这个成员,__rq_for_each_bio(_bio,rq)宏封装了对bio链表的遍历方法,其定义为:

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

                                         char *buffer:指向缓冲区的指针,数据应当被传送到获得来自这个缓冲区,这个指针是一个内核虚拟地址,可被驱动直接引用。

                                         unsigned short nr_phys_segments:该值表示相邻的页被合并后,这个请求在物理内存中占据的段的数目。

                                         如果设备支持分散/聚集(SG,scatter/gather)操作,可依据此字段申请sizeof(scatterlist) *nr_phys_segments的内存,并使用下列函数进行DMA映射:

                                                 int blk_rq_map_sg(struct request_queue *, struct request *, struct scatterlist *);

                                         该函数与dma_map_sg()类似,它返回scatterlist列表入口的数量。

                                         struct list_head queuelist; 用于链接这个请求到请求队列的链表结构,blkdev_dequeue_request()可用于从队列中移除请求。

                                         使用rq_data_dir(struct request *req);宏可以从request获得数据传动的方向,0返回值表示从设备中读,非0返回值表示向设备写。

                         2.请求队列

                                 一个块请求队列是一个块I/O request的队列。请求队列跟踪等候的块I/O请求,它存储用于描述这个设备更够支持的请求的类型信息,他们的最大大小,多少不同的段可以进入一个请求、硬件扇区大小、对齐等参数,其结果是:如果请求队列保被配置正确了,它不会交给该设备一个不能处理的请求。

                                 请求队列还实现一个插入接口,这个接口允许使用多个I/O调度器,I/O调度器(也称电梯)的工作室以最优性能的方式向驱动提交I/O请求,大部分I/O调度器累积批量的I/O请求,并将它们排列为递增(或递减)的块索引顺序后提交给驱动。进行这些工作的原因在于,对于磁头而言,当给定顺序排列的请求时,可以使得磁盘顺序地从一头到另一头工作,非常像一个满载的电梯,在一个方向移动直到所有它的“请求”被满足。

                                另外,I/O调度器还负责合并临近的请求,当一个新I/O请求被提交给调度器后,它会在队列里搜索包含临近扇区的请求。如果找到一个,并且如果结果的请求不是太大,调度器将合并这两个请求。

                                对磁盘等块设备进行I/O操作顺序的调度类似于电梯的原理,先服务完上楼的乘客,在服务下楼的乘客效率会跟高,而顺序响应用户的请求则电梯会无序地忙乱。

                                Linux 2.6内核包含4个I/O调度器,他们分别是No-op I/O scheduler、Anticipatory I/O scheduler、Deadline I/O scheduler与CFQ I/O scheduler。

                                Noop I/O scheduler是一个简化的调度程序,该算法实现了简单FIFO队列,它只作最基本的合并与排序。

                                Anticipatory I/O scheduler算法推迟I/O请求,以期能对它们进行排序,获得最高的效率。在每次处理完读请求之后,不是立即返回,而是等待几个微妙。在这段时间内,任何来自临近区域的请求都被立即执行。超时以后,继续原来的处理。

                                Deadline I/O scheduler是针对Anricipatory I/O scheduler的缺点进行改善而来的,它试图把每次请求的延迟将至最低,该算法重排了请求的顺序来提高性能。它使用轮训的调度器,简洁小巧,提供了最小的读取延迟和尚佳的吞吐量,特别适合于读取较多的环境(比如数据库)。

                                CFQ I/O scheduler为系统内的所有任务分配均匀的I/O带宽,提供一个公平的工作环境,在多媒体应用中,能保证audio、video及时从磁盘读取数据。

                                内核block目录中的noop-iosched.c、as-iosched.c、deadline-iosched.c和cfq-iosched.c文件分别实现了上述调度算法。

                                可以通过给kernel添加启动参数,选择使用的IO调度算法,如:kernel elevator=deadline。

                                (1)初始化请求队列

                                        request_queue_t *blk_init_queue(request_fn_proc *rfn, spinlock_t *lock);

                                        该函数的第一个参数是请求处理函数的指针,第二个参数是控制访问队列权限的自旋锁,这个函数会发生内存分配的行为,它可能会失败,因此一定要检查它的返回值。这个函数一般在块设备驱动的模块加载函数中调用。

                                (2)清除请求队列

                                         void blk_cleanup_queue(request_queue_t *q);

                                         这个函数完成将请求队列返回给系统的任务,一般在块设备驱动模块卸载函数中调用。

                                         而blk_put_queue(q) blk_cleanup_queue((q))

                                 (3)分配“请求队列”

                                         request_queue_t *blk_alloc_queue(int gfp_mask);

                                         对于Flash、RAM盘等完成随机访问的非机械设备,并不需要进行复杂的I/O调度,这个时候,应该使用上述函数分配一个“请求队列”,并使用如下函数来绑定请求队列和“制造请求”函数:

                                                 void blk_queue_make_request(request_queue_t *q, make_request_fn *mfn);

                                         这种方式分配的“请求队列”实际上不包含任何request,所以给其加上引号。

                                 (4)提取请求

                                         struct request *elv_next_request(struct request_queue *q);

                                         用于返回下一个要处理的请求(由I/O调度器决定),如果没有请求则返回NULL。

                                         elv_next_request()不会清除请求,它仍然将这个请求保留在队列上,但是标识它为活动的,这个标识将阻止I/O调度器合并其他的请求到已开始执行的请求。因为elv_next_request()不从队列里清除请求,因此连续调用它两次,两次会返回同一个请求结构体。

                                 (5)去除请求

                                         void blkdev_dequeue_request(struct request *req);

                                         从队列中去除一个请求。如果驱动中同时从同一个队列中操作了多个请求,它必须以这样的方式将它们从队列中去除。

                                         如果需要将一个已经出列的请求归还到队列中,可以进行以下调用:

                                                 void elv_requeue_request(request_queue_t *queue, struct request * req);

                                 另外,块设备层还提供了一套函数,这些函数可被驱动用来控制一个请求队列的操作,,主要包括以下操作。

                                 (6)启停请求队列

                                         void blk_stop_queue(request_queue_t *queue);

                                         void blk_start_queue(request_queue_t * queue);

                                         如果块设备到达不能处理等候的命令的状态,应调用blk_stop_queue()来告知块设备层,之后,请求函数将不被调用,除非再次调用blk_start_queue()将设备恢复到可处理请求的状态。

                                 (7)参数设置

                                         void blk_queue_max_sectors(request_queue_t *queue, unsigned short max);        //描述任一请求可包含的最大扇区数,默认值为255

                                         void blk_queue_max_phys_segments(request_queue_t *queue, unsigned short max); 控制一个请求可包含的最大物理段(系统内存中不相邻的区)

                                         void blk_queue_max_hw_segments(request_queue_t *queue, unsigned short max);  //考虑了系统I/O内存管理单元的重映射,缺省都是128

                                         void blk_queue_max_segment_size(request_queue_t *queue, unsigned int max);   //告知内核请求段的最大字节数,默认值为65536。

                                 (8)通告内核

                                         void blk_queue_bounce_limit(request_queue_t *queue, u64 dma_addr);

                                         该函数用于告知内核块设备执行DMA时可使用的最高物理地址dma_addr,如果一个请求包含超出这个限制的内存引用,系统将会给出这个操作分配一个“反弹”缓冲区,这种方式的代价昂贵,因此应尽量避免使用。

                                         可以给dma_addr参数提供任何可能的值或使用预先定义的宏,如BLK_BOUNCE_HIGH(对高端内存页使用反弹缓冲区)、BLK_BOUNCE_ISA(驱动只可在16MB的ISA区执行DMA)或者BLK_BOUNCE_ANY(驱动可在任何地址执行DMA),缺省值是BLK_BOUNCE_HIGH。

                                         blk_queue_segment_boundary(request_queue_t *queue, unsigned long mask);

                                         如果我们正在驱动编写的设备无法处理跨越一个特殊大小内存边界的请求,应该使用这个函数来告知内核这个边界。例如,如果设备处理跨4MB边界的请求有困难,应该传递一个0x3fffff掩码,缺省的掩码是0xffffff(对应4GB边界)。

                                         void blk_queue_dma_alignment(request_queue_t *queue, int mask);

                                         告知内核块设备施加于DMA传送的内存对齐限制,所有请求都匹配这个对齐,缺省的屏蔽式0x1ff,它导致所有的请求被对齐到512字节边界。

                                         void blk_queue_hardsect_size(request_queue_t *queue, unsigned short max);

                                         该函数告知内核块设备硬件扇区的大小,所有由内核产生的请求都是这个大小的倍数并且被正确对界,但是,内核块设备层和驱动层之间的通信还是以512字节扇区为单位进行。

                         3.块I/O

                                 通常一个bio对应一个上层传递给块层的I/O请求。I/O调度算法可将连续的bio合并成一个request。request是bio经由块层进行调整后的结果,这是request和bio的区别,所以,一个request可以包含多个bio。

                                 下面我们对bio的核心成员进行分析:

                                         sector_t bi_sector;        标识这个bio要传送的第一个(512字节)扇区。

                                         unsigned int bi_size;        被传送的数据大小,以字节为单位,驱动中可以使用bio_sector(bio)宏获得以扇区为单位的大小,该宏实际定义为((bio)->bi_size>>9)。

                                         unsigned long bi_flags;    unsigned long bi_rw;    一组描述bio的标志,如果这是一个写请求,bi_rw最低有效为被置位,可以使用bio_data_dir(bio)宏来获得读写方向,该宏实际定义为((bio)->bi_rw&1)。

                                  bio的核心是一个称为bi_io_vec的数组,它由bio_vec结构体组成。bio_vec结构体的定义如下:

                                          struct bio_vec{

                                                  struct page *bv_page;        /*页指针*/

                                                  unsigned int bv_len;        /*传输的字节数*/

                                                  unsigned int bv_offset;        /*偏移位置*/

                                          };

                                  我们不应该直接访问bio的bio_vec成员,而应该使用bio_for_each_segment()宏来进行这项工作,可以用这个宏循环遍历整个bio中的每个段,这个宏的定义如下:

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

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

                                  下图(a)所示为request队列、request与bio数据结构之间的关系,(b)所示为request、bio和bio_vec数据结构之间的关系,(c)所示为bio与bio_vec数据结构之间的关系,因此下图递归呈现了request队列、request、bio和bio_vec这4个结构体之间的关系。

                                          Linux设备驱动实例

 

                              Linux设备驱动实例

                                 Linux设备驱动实例

                                内核还提供了一组函数(宏)用于操作bio:

                                        int bio_data_dir(struct bio* bio);    这个函数可以用于获得数据传输的方向是READ还是WRITE。

                                        struct page *bio_page(struct bio* bio);    这个函数可用于获得目前的页指针。

                                        int bio_offset(struct bio* bio);        这个函数返回同操作对应的的当前页内的偏移,通常块I/O操作本身就是页对齐的。

                                        int bio_cur_sectors(struct bio* bio);    这个函数返回当前bio_vec要传输的扇区数。

                                        char *bio_data(struct bio* bio);    这个函数返回数据缓冲区的内核虚拟地址。

                                        char *bvec_kmp_irq(struct bio_vec *bvec, unsinged long *flags);    这个函数返回一个内核虚拟地址,这个地址可用于存取被给定的bio_vec入口指向的数据缓冲区,它也会屏蔽中断并返回一个原子kmap(用于高端内存映射),因此,在bvec_kunmap_irq()被调用以前,驱动不因该睡眠。

                                        void bvec_kunmap_irq(char *buffer, unsigned long * flags);    这个函数是bvec_kmap_irq()函数的“反函数”,它撤销bvec_kmap_irq()创建的映射。

                                        char* bio_kmap_irq(struct bio *bio, unsigned long *flags);    这个函数是对bvec_kmap_irq()的包装,它返回给定的bio的当前bio_vec入口的映射。

                                        char* __bio_kmap_atomic(struct bio* bio, int i, enum km_type type);    这个函数通过kmap_atomic()获得返回给定bio的第i个缓冲区的虚拟地址。

                                        void __bio_kunmap_atomic(char *addr, enum km_typr type);    这个函数返还由__bio_kmap_atomic()获得的内核虚拟地址。

                                需要注意的是,xx_kmap_xx、xx_kunmap_xx系列函数针对的是支持高端内存的驱动。另外,对bio的引用计数通过如下宏/函数完成:

                                        #define bio_get(bio)    atmic_inc(&(bio)->bio_cnt)

                                        void bio_put(struct bio *bio);    /*释放对bio的引用*/

                                如下函数对于在内核中向块层提交一个BIO:

                                       void submit_bio(int rw, struct bio* bio);

                                结合使用bio_get()、sunmit_bio()、bio_put()的流程一般是:

                                        bio_get(bio);

                                        submit_bio(rw, bio);

                                        if(bio->bi_flags ...)

                                                do_something

                                        bio_put(bio);

                13.2.4 块设备驱动注册与注销

                        块设备驱动中的第一个工作通常是注册他们自己的内核,完成这个任务的函数是register_blkdev(),其原型为:

                                int register_blkdev(unsigned int major, const char * name);

                        major参数是块设备要使用的主设备号,name为设备名,它会在/proc/devices中被显示。如果major为0,内核会自动分配一个新的设备号,register_blkdev()函数的返回值就是这个主设备号。如果register_blkdev()返回一个负值,表明发生了一个错误。

                        与register_blkdev()对应的注销函数是unregister_blkdev(),其原型为:

                                int unregister_blkdev(unsigned int major, const char *name);

                        这里,传递给unregister_blkdev()的参数必须与传递给register_blkdev()的参数匹配,否则这个函数返回-EINVAL。

                        值得一提的是,在Linux 2.6内核中,对register_blkdev()的调用完全是可选的,register_blkdev()的功能已随时间正在减少,这个调用最多只完成两件事。

                                (1)如果需要,分配一个动态主设备号。

                                (2)在/proc/devices中创建一个入口。

                        在将来的内核中,register_blkdev()可能会被去掉。但是目前大部分驱动仍然调用它。

        13.3 Linux块设备驱动的模块加载和卸载

                在块设备驱动的模块加载函数中通常需要完成如下工作。

                        (1)分配、初始化请求队列,绑定请求队列和请求函数。

                        (2)分配、初始化gendisk,给gendisk的major、fops、queue等成员赋值,最后添加gendisk。

                        (3)注册块设备驱动。

                在块设备驱动的模块卸载函数中完成与模块加载函数相反的工作。

                        (1)清除请求队列。

                        (2)删除gendisk和对gendisk的引用。

                        (3)删除对块设备的引用,注销块设备驱动。

        13.4 块设备的打开与释放

                块设备驱动的open()和release()函数并非是必须的,一个简单的块设备驱动可以不提供open()和release()函数。

                块设备驱动的open()函数和其字符设备驱动的对等体不太相似,不以相关的inode和file结构体指针作为参数。在open()中我们可以通过block_device参数bdev获取private_data、在release()函数中则通过gendisk参数disk获取。

                在一个处理真实的硬件设备的驱动中,open()和release()方法还应当设置驱动和硬件的状态,这些工作可能包含启停磁盘、加锁一个可移出设备和分配DMA缓冲等。

        13.5 块设备驱动的ioctl函数

                与字符设备驱动一样,块设备可以包含一个ioctl()函数以以提供对设备的I/O控制能力。实际上,高层的块设备层代码处理了绝大多数I/O控制,如BLKFISBUF、BLKROSET、BLKDISCARD、HDIO_GETGEO、BLKROGET、BLKSECTGET等,因此,具体的块设备驱动中通常只需要实现设备相关的ioctl命令。例如,源代码文件为drivers/block/floppy.c中实现了与软驱相关的命令如FDEJECT、FDSETPRM、FDFMTTRK等。

        13.6 块设备驱动的I/O请求处理

                13.6.1 使用请求队列

                        块设备驱动请求函数的原型为:void request(request_queue_t *queue);

                        这个函数不能由驱动自己调用,只有当内核认为是时候让驱动处理对设备的读写等操作时,它才调用这个函数。

                        请求函数可以在没有完成请求队列中的所有请求的情况下返回,甚至在一个请求不完成都可以返回。但是,对大部分设备而言,在请求函数中处理完所有请求后再返回通常是值得推荐的方法,

                        当设备已经完成一个I/O请求的部分或者全部扇区传输后,它必须通告块设备层,__end_that_request_first()函数的原型为:

                                int __end_that_request_first(struct request *req, int error, int nr_bytes);

                        这个函数告知块设备层,块设备驱动已经完成nr_bytes个扇区的传送。__end_that_request_first()的返回值是一个标志,指示是否这个请求中的所有扇区已经被传送。返回值为0表示所有的扇区已经被传送并且这个请求完成,之后,我们必须使用blkdev_dequeue_request()来从队列中清除这个请求。最后,将这个请求传递给end_that_request_last()函数。

                                void end_that_request_last(struct request *req);

                        end_that_request_last()通知所有正在等待这个请求完成的对象请求已经完成并回收这个请求结构体。

                        add_disk_randomness()函数的作用是使用块I/O请求的时间信息给系统的随机数池贡献。它不影响块设备驱动,但是,仅当磁盘的操作时间是真正随机的时候(gendisk的random成员不为0,大部分机械设备如此),它才会调用add_timer_randomness()。

                        rq_for_each_segment()实际是一个二重循环,它的第一重循环遍历一个request的每个bio,第二重循环遍历一个bio的每个segment:

                                #define rq_for_each_segment(bvl, _rq, _iter)  __rq_for_each_bio(_iter.bio, _rq)  bio_for_each_segment(bvl, _iter,bio, _iter.i)

                        下图所示为一个请求队列内request、bio以及bio中segment的层层遍历关系。

                                Linux设备驱动实例

                13.6.2 不使用请求队列

                        使用请求队列对于一个机械的磁盘设备而言的确有助于提高系统的性能,但是对于许多块设备,如数码相机的存储卡、RAM盘等完全可真正随机访问的设备而言,无法从高级的请求队列逻辑中获益。对于这些设备,块层支持“无队列”的操作模式,为使用这个模式,驱动必须提供一个“制造请求”函数,而不是一个请求函数,“制造请求”函数的原型为:

                                typedef int (make_erquest_fn)(request_queue_t *q, struct bio *bio);

                        上述函数的一个参数仍然是“请求队列”,但是这个“请求队列”实际不包含任何request,因为块层没有必要将bio调整为request。因此,“制造请求”函数的主要参数是bio结构体,这个bio结构体表示一个或多个要传送的缓冲区。“制造请求”函数或者直接进行传输,或者把请求重定向给其他设备。

                        在“制造请求”函数中处理bio的方式与13.6.1小节讲解的完全一致,但是在处理完成后应该使用bio_endio()函数通知处理结束,函数原型如下所示:

                                void bio_endio(struct bio *bio, unsigned int bytes, int error);

                        参数bytes是已经传送的字节数,它可以比这个bio所代表的字节数少,这意味着“部分完成”,同时bio结构体中的当前缓冲区指针需要更新。当设备进一步处理这个bio后,驱动应该再次调用bio_endio(),如果不能完成这个请求,应指出一个错误,错误码赋值给error参数。

                        不管对应的I/O处理成功与否,“制造请求”函数都应该返回0。如果“制造请求”函数返回一个非零值,bio将被再次提交。

        13.7 实例1:vmem_disk驱动

        13.8 实例2:IDE硬盘设备驱动

14 Linux终端设备驱动

        14.1 终端设备

                在Linux系统中,终端是一种字符型设备,它有多种类型,通常使用tty来简称各种类型的终端设备。tty是Teletype的缩写,Teletype是最早出现的一种终端设备,很像电传打字机,是由teletype公司生产的。Linux中包含如下几类终端设备。

                1.串行端口终端(/dev/ttySn)

                         串行端口终端(Serial Port Terminal)是使用计算机串行端口连接的终端设备。计算机把每个串行都看作是一个字符设备。这些串行端口所对应的设备名称是/dev/ttyS0(或/dev/tts/0)、/dev/ttyS1(或/dev/tts/1)等,设备号分别是(4,0)、(4,1)等。
                         在命令行上把标准输出重定向到端口对应的设备文件上就可以通过该端口发送数据,例如,在命令行提示符下键入:echo test>/dev/ttyS1会把单词“test”发送到连接在ttyS1端口的设备上。

                         目前USB串口转换器也已经非常常用,其对应设备结点通常为/dev/ttyUSB0、/dev/ttyUSB1等。

                2.伪终端

                        伪终端pty(Pseudo Terminal)是成对的逻辑终端设备,并存在成对的设备文件,如/dev/ptyp3和/dev/ttyp3,他们与实际物理设备并不直接相关,如果一个程序把ttyp3看作是一个串行端口设备,则它对该端口的读/写操作会反映在该逻辑终端设备对应的ttyp3上,而ttyp3则是另一个程序用于读写操作的逻辑设备。这样,两个程序就可以通过这种逻辑设备进行相互交流,使用ttyp3的程序会认为自己正在与一个串行端口进行通行。

                3.控制终端(/dev/tty)

                        如果当前进程有控制终端(Controlling Terminal)的话,那么/dev/tty就是当前进程的控制终端的设备特殊文件。可以使用命令“ps -ax”来查看进程与哪个控制终端相连,使用命令“tty”可以查看它具体对应哪个实际终端设备。/dev/tty有些类似于到实际所使用终端设备的一个link。

                        内核线程(如kthreadd)和用户空间的守护进程(如udevd)是没有控制终端的。

                4.控制台终端(/dev/ttyn, /dev/console)

                        在UNIX系统中,计算机显示器通常被称为控制台终端(console),它仿真了类型为Linux的一种终端(TERM=Linux),并且有一些设备特殊文件与之关联:tty0、tty1、tty2等。当用户在控制台上登录时,使用的是tty1。使用Alt+[F1~F6]组合键时,我们就可以切换到tty2、tty3等上面去。tty1~tty6等称为虚拟终端,而tty0则是当前所使用虚拟终端的一个别名,系统所产生的信息会发送到该终端上,因此不管当前正在使用哪个虚拟终端,系统信息都会发送到该控制台终端上。用户可以登录到不同的虚拟终端上去,因而可以让操作系统同时有几个不同的会话期存在。只有系统或超级用户root可以向dev/tty0进行写操作。

                在Linux中,可以在系统启动命令行里指定当前的输出终端,格式如下:

                       console=device, options

                device指代的是终端设备,可以是tty0(前台的虚拟终端)、ttyX(第X个虚拟终端)、ttySX(第X个串口)、lp0(第一个并口)等。

                options指代对device进行的设置,它取决于具体的设备驱动,对于串口设备,参数用来定义为:波特率、校验位、位数,格式为BBBBPN,其中BBBB表示波特率,P表示校验(n/o/e),N表示位数,默认options是9600n8.

                用户可以在内核命令行中同时设定多个console,这样输出将会在所有的console上显示,而当用户调用open()打开/dev/console时,最后一个console将会返回作为当前值。例如:

                        console=ttyS1, 9600 console=tty0

                定义了两个console,而调用open()打开/dev/console时,将使用虚拟终端tty0.但是内核消息会在tty0 VGA虚拟终端和串口ttyS1上同时显示。简单地说,我们可以把/dev/console看作内核控制台的tty文件接口,设备号位0x0501,当对其调用tty_open()时,它会转义为实际的终端设备。

                通过查看/proc/tty/drivers文件可以获知什么类型的tty设备存在以及什么驱动被加载到内核,这个文件包括一个当前存在的不同tty驱动的列表,包括驱动名、缺省的节点名、驱动的主编号、这个驱动使用的次编号范围,以及tty驱动的类型。

        14.2 终端设备驱动结构

                Linux内核中tty的层次结构如下图,包含tty核型、tty线路规程和tty驱动、tty线路规程的工作是以特殊的方式格式化从一个用户或者硬件收到的数据,这种格式化常常采用一个协议转换的形式,例如PP和Bluetooth。tty设备发送数据的流程为:tty核心从一个用户获取将要发送给一个tty设备的数据,tty核心将数据传递给tty线路规程驱动,接着数据被传递到tty驱动,tty驱动将数据转换为可以发送给硬件的格式,接收数据的流程为:从tty硬件接收到的数据向上交给tty驱动,进入tty线路规程驱动,进入tty线路规程驱动,在进入tty核心,在这里它被一个用户获取。尽管大多数时候tty核心和tty之间的数据传输会经历tty线路规程的转换,但是tty驱动与tty核心之间也可以直接传输数据。

                        Linux设备驱动实例

                下图显示了与tty相关的主要源文件及数据的流向。drivers/char/tty_io.c定义了tty设备通用的file_operations结构体并实现了接口函数tty_register_driver()用于注册tty设备,它会利用fs/char_dev.c提供的接口函数注册字符设备,与具体设备对应的tty驱动将实现tty_driver结构体中的成员函数。同时tty_io.c也提供了tty_register_ldisc()接口函数用于注册线路规程,典型地,例如drivers/char/n_tty.c文件则针对N_TTY线路规程实现了tty_disc结构体中的成员。

                        Linux设备驱动实例

                 从上图可以看出,特定tty设备驱动的主体工作是填充tty_driver结构体中的成员,实现其中的成员函数。

                 tty_driver结构体中的magic表示给这个结构体的"幻数",设为TTY_DRIVER_MAGIC(即0x5402),在alloc_tty_driver()函数中被初始化。

                 name与driver_name的不同在于后者表示驱动的名字,用在/proc/tty和sysfs中,而前者表示驱动的设备节点名。

                 type与subtype描述tty驱动的类型和子类型,subtype的值依赖于type,type成员的可能值为TTY_DRIVER_TYPE_SYSTEM(subtype应当设为SYSTEM_TYPE_TTY、SYSTEM_TYPE_CONSOLE、SYSTEM_TYPE_SYSCONS或SYSTEM_TYPE_SYSPTMX)、TTY_DRIVER_TYPE_CONSOLE(仅被控制台驱动使用)、TTY_DRIVER_TYPE_SERIAL(被任何串行类型驱动使用,subtype通常设为SERIAL_TYPE_NORMAL)、TTY_DRIVER_TYPE_PTY(被伪控制台接口pty使用,此时subtype需要被设置为PTY_TYPE_MASTER或PTY_TYPE_SLAVE)、TTY_DRIVER_TYPE_SCC(由SCC驱动使用)。

                init_termios为初始线路设置,为一个termios结构体,这个成员被用来提供一个线路设置集合。

                termios用于保存当前的线路设置,这些线路设置控制当前波特率、数据大小、数据流控设置等,这个结构体包含tcflag_t c_iflag(输入模式标志)、tcflag_t c_oflag(输出模式标志)、tcflag_t c_cfalg(控制模式标志)、tcflag_t c_lflag(本地模式标志)、cc_t c_line(线路规程类型)、cc_t c_cc[NCCS](一个控制字符数组)等成员。

                驱动会使用一个标准的数值集初始化这个成员,它拷贝自tty_std_termios变量。

                tty_driver结构体中的major、minor_start、minor_num表示主设备号、次设备号及可能的次设备数,name表示设备名(如ttyS)。

                put_char()为单字节写函数,当单个字节被写入设备时这个函数被tty核心调用,如果一个tty驱动没有定义这个函数,将使用count参数为1的write函数。

                flush_chars()与wait_until_seng()函数都用于刷新数据到硬件。

                write_room()指示有多少缓冲区空闲,chars_in_buffer()指示缓冲区中包含的数据数。

                当在tty设备的设备节点上执行IOCTL操作时,tty_operations结构体的ioctl()函数会被tty核心调用。

                当设备的termios设置被改变时,set_termios()函数将被tty核心调用。

                throttle()、unthrottle()、stop()和start()为数据抑制函数,这些函数用来辅助控制tty核型的输入缓冲区。当tty核心的输入缓冲区满时,throttle()函数将被调用,tty驱动试图通知设备不应当发送字符给它。当tty核心得输入缓冲区已被清空时,unthrottle()函数将被调用以暗示设备可以接收数据。stop()和start()函数非常像throttle()和unthrottle()函数,但它们表示tty驱动应当停止发送数据给设备以及恢复发送数据。

                当tty驱动挂起tty设备时,hangup()函数被调用,在此函数中进行相关的硬件操作。

                当tty驱动要在RS-232端口上打开或关闭线路的BREAK状态时,break_ctl()线路中断控制函数被调用。如果state状态设为-1,BREAK状态打开,如果状态设为0,BREAK状态关闭。如果这个函数由tty驱动实现,而tty核心将处理TCSBRK、TCSBRKP、TIOCSBRK和TIOCCBRK这些IOCTL命令。

                flush_buffer()函数用于刷新缓冲区并丢弃任何剩下的数据。

                set_ldisc()函数用于设置线路规程,当tty核心改变tty驱动的线路规程时这个函数被调用,这个函数通常不需要被驱动定义。

                send_xchar()为X-类型字符发送函数,这个函数用来发送一个高优先级XON或者XOFF字符给tty设备,要被发送的字符在第2个参数ch中指定。

                read_proc()和write_proc()为/proc读和写函数。

                tiocmget()函数用于获得tty设备的线路设置,对应的tiocmset()用于设置tty设备的线路设置,参数set和clear包含了要设置或者清除的线路设置。

                Linux内核提供了一组函数用于操作tty_driver结构体及tty设备,如下所示:

                        (1)分配tty驱动

                                struct tty_driver *alloc_tty_driver(int lines);

                                这个函数返回tty_driver指针,其参数为要分配的设备数量,line会被复制给tty_driver的num成员。

                        (2)注册tty驱动

                                int tty_register_driver(struct tty_driver *driver);

                                参数为由alloc_tty_driver()分配的tty_driver结构体指针,注册tty驱动成功时返回0。

                        (3)注销tty驱动

                                int tty_unregister_driver(struct tty_driver *driver);

                                这个函数与tty_register_driver()对应,tty驱动最终会调用上述函数注销tty_driver。

                        (4)注册tty设备

                                void tty_register_device(struct tty_driver *driver, unsigned index, struct device *device);

                                仅有tty_driver是不够的,驱动必须依赖于设备,tty_register_device()函数用于注册关联于tty_dricer的设备,index为设备的索引(范围是0~driver->num),如:for(i=0; i < XXX_TTY_MINORS; ++i)        tty_register_device(xxx_tty_dricer, i, NULL);

                        (5)注销tty设备

                                void tty_unregister_device(struct tty_driver *driver, unsigned index);

                                上述函数与tty_register_device()对应,用于注销tty设备,其使用方法如:for(i=0; i<XXX_TTY_NINORS; ++i)    tty_unregister_device(xxx_tty_device, i);

                        (6)设置tty驱动操作

                                void tty_set_operations(struct tty_driver *driver, struct tty_operations *op);

                                上述函数会将tty_operations结构体中的函数指针拷贝给tty_driver对应的函数指针。

                        终端设备驱动都围绕tty_driver结构体而展开,一般而言,终端设备驱动应包含如下组成。

                                (1)终端设备驱动模块架子啊函数和卸载函数,完成注册和注销tty_driver,初始化和释放终端设备对应的tty_driver结构体成员及硬件资源。

                                (2)实现tty_operations结构体中的一系列成员函数,主要是实现open()、close()、write()、tiocmget()、tiocmset()等函数。

        14.3 终端设备驱动初始化与释放

                14.3.1 模块加载与卸载函数

                        tty驱动的模块加载函数中通常需要分配、初始化tty_driver结构体并申请必要的硬件资源。tty驱动的模块卸载函数完成与模块加载函数相反的工作。

                14.3.2 打开与关闭函数

                        当用户对tty驱动所分配的设备节点进行open()系统调用时,tty_driver所拥有的tty_operations中的open()成员函数将被tty核心调用。tty驱动必须设置open()成员,否则,——ENODEV将被返回给调用open()的用户。

                        open()成员函数的第1个参数为一个指向分配给这个设备的tty_struct结构体的指针,第2个参数为文件指针。

                        open()成员函数的第1个参数为一个指向分配给这个设备的tty_struct结构体指针,第2个参数为文件指针。

                        tty_struct结构体被tty核心用来保存当前tty端口的状态,它的大多数成员只被tty核心使用。tty_struct中的几个重要成员如下。

                                (1)flags标示tty设备的当前状态,包括TTY_THROTTLED、TTY_IO_ERROR、TTY_OTHER_CLOSED、TTY_EXCLUSIVE、TTY_DEBUG、TTY_DO_WRITE_WAKEUP、TTY_PUSH、TTY_CLOSING、TTY_DONT_FLIP、TTY_HW_COOK_OUT、TTY_HW_COOK_IN、TTY_PTY_LOCK、TTY_NO_WRITE_SPLIT等。

                                (2)ldisc为给tty设备的线路规程。

                                (3)write_wait、read_wait为给tty写/读函数的等待队列,tty驱动应当在合适的时机唤醒对应用的等待队列。

                                (4)termios为指向tty设备的当前termios设置的指针。

                                (5)stopped:1指示是否停止tty设备,tty驱动可以设置这个值;hw_stopped:1指示是否tty设备已经被停止,tty驱动可以设置这个值;flow_stopped:1指示是否tty设备数据流停止。

                                (6)driver_data、disc_data为数据指针,用于存储tty驱动和线路规程的“私有”数据。

                         驱动中可定义一个设备相关的结构体,并在open()函数中将其赋值给tty_struct的driver_data成员。在用户对前面使用open()系统调用而创建的文件句柄进行close()系统调用时,tty_driver所拥有的tty_operations中的close()成员函数将被tty核心调用。

        14.4 数据发送和接收

                下图所示终端设备数据发送和接收过程中的数据流以及函数调用关系。用户在有数据发送给终端设备时,通过“write()系统调用--tty核心--线路规程”的层层调用,最终调用tty_driver结构体中的write()函数完成发送。因为传输速度和tty硬件缓冲区容量的原因,不是所有的写程序要求的字符都可以在调用写函数时被发送,因此写函数应当返回能够发送给硬件的字节数以便用户程序检查是否所有的数据被真正写入。如果在write()调用期间发生任何错误,一个负的错误码应当被返回。

                        Linux设备驱动实例

                tty_driver的write()函数接受3个参数tty_struct、发送数据指针及要发送的字节数,一般首先会通过tty_struct的driver_data成员得到设备私有信息结构体,然后依次进行必要的硬件操作开始发送。

                当tty子系统自己需要发送数据到tty设备时,如果没有实现put_char()函数,write()函数将被调用,此时传入的count参数为1。

                当tty_operations结构体没有提供read()函数。因为发送是用户主动的,而接收即用户调read()则是读一片缓冲区中已放好的数据。tty核心在一个称为struct tty_flip_buffer的结构体中缓冲数据直到他被用户请求。因为tty核心提供了缓冲逻辑,因此每个tty驱动并非一定要实现它自身的缓冲逻辑。

                tty驱动不必过于关心tty_flip_buffer结构体的细节,如果其count字段大于或等于TTY_FLIPBUF_SIZE,这个flip缓冲区就需要被刷新到用户,刷新通过对tty_flip_buffer_push()函数的调用来完成。

                从tty驱动接收到字符被tty_insert_flip_char()函数插入flip缓冲区,该函数的第1个参数是数据应当保存入的tty_struct结构体,第2个参数是要保存的字符,第3个参数是应当为这个字符设置的标志,如果字符是一个接收到的常规字符,则设为TTY_NORMAL,如果是一个特殊类型的指示错误的字符,依据具体的错误类型,应当设为TTY_BREAK、TTY_TARITY或TTY_OVERRUN。

        14.5 TTY线路设置

                14.5.1 线路设置用户空间接口

                        用户可用如下两种方式改变tty设备的线路设置或者获取当前线路设置。

                        1.调用用户空间的termios库函数

                                用户空间的应用程序需引用termios.h头文件,该头文件包含了终端设备的I/O接口,实际是由POSIX定义的标准方法,对终端设备操作模式的描述由termios结构体完成。这个结构体包含c_iflag、c_oflag、c_lflag和c_cc[]几个成员。

                                termios的c_cflag主要包含如下位域信息:CSIZE(字长)、CSTOPB(两个停止位) 、PARENB(奇偶校验位使能)、PARODD(奇校验位,当PARENB被使能时)、CREAD(字符接收使能,如果没有置位,让然从端口接收字符,但这些字符都要被丢弃)、CRTSCTS(如果被置位,使能CTS状态改变报告)、CLOCAL(如果没有置位,使能调制解调器状态改变报告)。

                                termios的c_iflag主要包含如下位域信息:INPCK(使能帧和奇偶校验错误检查)、BRKINT(break将清除终端输入/输出队列,向该终端上前台的程序发出SIGINT信号)、PARMARK(奇偶校验和帧错误被标记,在INPCK被设置切IGNPAR未被设置的情况下才有意义)、IGNPAR(忽略奇偶校验和帧错误)、IGNBRK(忽略break)。

                                通过tcgetattr()、tcsetattr()函数即可完成对终端设备的操作模式的设置和获取,这两个函数的原型如下:

                                        int tcgetattr(int fd, struct termios *termios_p);

                                        int tcsgetattr(int fd, int optional_actions, struct termios *termios_p);

                                例如,Raw模式的线路设置如下。

                                        非正规模式。

                                        关闭回显。

                                        禁止CR到NL的映射(ICRNL)、输入奇偶校验、输入第8位的截取(ISTRIP)以及输出流控制。

                                        8位字符(CS8),奇偶校验被禁止。

                                        禁止所有的输出处理。

                                        每次一个字节(c_cc[VMIN]=1、c_cc[VTIME]=0)。

                                则对应的对termios结构体的设置就为:

                                        termios_p->c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL | IXON);

                                        termios_p->c_oflag &= ~OPOST;

                                        termios_p->c_lflag &= ~(ECHO | ECHONL | ICANON | ISIG | IEXTEN);

                                        termios_p->c_cflag &= ~(CSIZE | PARENB);

                                        termios_p->c_iflag |= CS8;

                                通过如下一组函数可完成输入/输出波特率的获取和设置:

                                        speed_t cfgetospeed(struct termios *termios_p);        //获得输出波特率

                                        speed_t cfgetispeed(struct termios *termios_p);        //获得输入波特率

                                        int cfsetospeed(struct termios *termios_p, speed_t speed);        //设置输出波特率

                                        int cfsetispeed(struct termios *termios_p, speed_t speed);        //设置输入波特率

                                如下一组函数则完成线路控制:

                                        int tcdrain(int fd);        //等待所有输出都被发送

                                        int tcflush(int fd, int queue_selector);        //flush输入/输出缓冲区

                                        int tcflow(int fd, int action);        //对输入和输出流进行控制

                                        int tcsendbreak(int fd, int duration);        //发送break

                                tcflush函数刷清(抛弃)输入缓冲区(终端驱动程序已接收到,但用户程序尚未读取)或输出缓冲区(用户程序已经写,但驱动尚未发送),queue参数可取TCIFLUSH(刷清除入队列)、TCOFLUSH(刷清输出队列)或TCIOFLUSH(刷清输入、输出队列)。

                                tcflow()对输入输出进行流控制,action参数可取TCOOFF(输出被挂起)、TCOON(重新启动以前被挂起的输出)、TCIOFF(发送1个STOP字符,使终端设备暂停发送数据)、TCION(发送一个START字符,使终端恢复发送数据)。

                                tcsendbreak()函数在一个指定的时间区间内发送连续的二进位数0。若duration参数为0,则此种发送延续0.25~0.5秒。POSIX.1说明duration非0,则发送时间以来于实现。

                        2.对tty设备节点进行ioctl()调用

                                大部分termios库函数会被转化为对tty设备节点的ioctl()调用,例如tcgetattr()、tcsetattr()函数对应着TCGETS、TCSETS IO控制命令。

                                TIOCMGET(获得MODEM状态位)、TIOCMSET(设置MODEM状态位)、TIOCMBIC(清除指示MODEM位)、TIOCMBIS(设置指示MODEM位)这4个I/O控制命令用于获取和设置MODEM握手,如RTS、CTS、DRT、DSR、RI、CD等。

                14.5.2 tty驱动set_termios函数

                         大部分termios用户空间函数被库转换为对驱动节点的ioctl调用,而tty_ioctl中的大部分命令会被tty核心转换为对tty驱动的set_termios函数的调用。set_termios函数需要根据用户对termios的设置(termios设置包括字长、奇偶校验位、停止位、波特率等)完成实际的硬件设置。

                         tty_operations中的set_termios函数原型为:void(*set_termios)(struct tty_struct *tty, struct termios *old);

                         新的设置被保存在tty_struct中,旧的设置被保存在old参数中,若新旧参数相同,则什么都不需要做,对于被改变的设置,需完成硬件上的设置。

                14.5.3 tty驱动的tiocmget和ticmset函数

                        对TIOCMGET、TIOCMSET、TIOCMBIC和TIOCMBIS IO控制命令的调用将被tty核心转换为tty驱动tiocmget函数和tiocmset函数的调用,TIOCMGET对应tiocmget函数,TIOCMSET、TIOCMBIC和TIOCMBIS对应tiocmset函数,分别用于读取Modem控制的设置和进行Modem的设置。

                        tiocmget函数会访问MODEM状态寄存器(MSR),而tiocmset()函数会访问MODEM控制寄存器(MCR)。

                14.5.4 tty驱动ioctl函数

                        当用户在tty设备节点上进行ioctl()调用时,tty_operation中的ioctl函数会被tty核心调用。如果tty驱动不知道如何处理传递给它的IOCTL值,它返回-ENOIOCTLCMD,之后tty核心会执行一个通用的操作。

                        驱动中常见的需处理的I/O控制命令包括TIOCSERGETLSR(获得这个tty设备的线路状态寄存器LSR的值)、TIOCGICOUNT(获得中断计数)等。

        14.6 UART设备驱动

                尽管一个特定的UART设备驱动完全可以遵循第12.2~14.5节的方法来设计,即定义tty_driver并实现tty_operations其中的成员函数,但是Linux已经在文件serial_core.c中实现了UART设备的通用tty驱动层(姑且称其为串口核心层)这样,UART驱动的主要任务演变成实现serial-core.c中定义的一组uart_xxx接口而非tty_xxx接口。如下图所示:

                        Linux设备驱动实例

                serial_cor.c串口核心层完全可以被当作14.2~14.5节tty设备驱动的实例,它实现了UART设备的tty驱动,回过头来再看12.2节“设备驱动的分层思想”,是否更加豁然开朗。

                串口核心层为串口设备驱动提供了如下3个结构体

                1.uart_driver

                        uart_driver包含串口设备的驱动名、设备名、设备号等信息,它封装了tty_driver。使得底层的UART驱动无需关心tty_driver。

                        一个tty驱动必须注册/注销tty_driver,而一个UART驱动则演变为注册/注销uart_driver,使用如下接口:

                                int uart_register_driver(struct uart_driver *dev);

                                void uart_unregister_driver(struct uart_driver *dev);

                        实际上,uart_register_driver()和uart_unregister_driver()中分别包含了tty_register_driver()和tty_unregister_driver()的操作。

                2.uart_port

                        uart_port用于描述一个UART端口(直接对应于一个串口)的I/O端口或I/O内存地址、FIFO大小、端口类型等信息。

                        串口核心层提供如下函数来添加一个端口:

                               int uart_add_one_port(struct uart_driver *drv, struct uart_port *port);

                        对上述函数的调用应该发生在uart_register_driver()之后,uart_add_one_port()的一个最重要作用是封装了tty_register_device()。

                        uart_add_one_port()的“反函数”是uart_remove_driver *drv, struct uart_port *port);

                3.uart_ops

                        uart_ops定义了针对UART的一系列操作,包括发送、接收及线路设置等,如果说tty_driver中tty_operations对于串口还较为抽象,那么uart_ops则直接面向了串口的UART,Linux驱动的这种层次非常类似于面向对象编程中基类、派生类的关系,派生类针对特定的事物会更加具体,而基类则站在更高的抽象层次上。

                        serial_core.c中定义了tty_operations的实例,包含uart_open()、uart_close()、uart_write()、uart_send_xchar()等成员函数,这些函数会借助uart_ops结构体中的成员函数完成具体的操作。

                        在使用串口核心层这个通用串口tty驱动层的接口后,一个串口驱动要完成的主要工作如下:

                                (1)定义uart_driver、uart_ops、uart_port等结构体的实例并在适当的地方根据具体硬件和驱动的情况初始化它们,当然具体设备xxx的驱动可以将这些结构套在新定义的xxx_uart_driver、xxx_uart_ops、xxx_uart_port之内。

                                (2)在模块初始化时调用uart_register_driver和uart_add_one_port以注册UART驱动并添加端口,在模块卸载时调用uart_unregister_driver和uart_remove_one_port以注销UART驱动并移除端口。

                                (3)根据具体硬件的datasheet实现uart_ops中的成员函数,这些函数的实现成为UART驱动的主体工作。

        14.7 printk和early_printk console驱动

                在Linux内核中,printk函数是最常用的调试手段,printk的打印消息会放入一个环形缓冲区,而/proc/kmsg文件用于描述这个缓冲区。通过dmesg命令或klogd可以读取该缓冲区,如果用户空间的klogd守护进程在运行,它将获取内核消息并分发给syslogd,syslog接着检查/etc/syslog.conf来找出如何处理它们。

                内核printk信息支持8个级别,从高到低分别是:KERN_EMERG、KERNEL_ALERT、KERN_CRIT、KERN_WARNING、KERN_NOTICE、KERN_INFO、KERN_DEBUG。当调用printk函数时指定的优先级小于指定的控制台优先级console loglevel时,调试信息就显示在控制台终端上,缺省的console loglevel值是DEFAULT SONSOLE,用户可以使用系统调用sys_syslog或klogd-c来修改console loglevel值,也可以直接echo值到/proc/sys/kernel/printk。/proc/sys/kernel/printk文档包含4个整数值,前两个表示系统当前的优先级和缺省优先级。

                在Linux中,用于printk输出的是内核console,专门用console结构体来描述。其中较关键的是write和setup成员函数,前者用于将打印消息写入console,后者用于设置console的特性,如波特率、停止位等。

                printk函数经过重重调用,经过__call_console_drivers函数,最终调用console的write成员函数将控制台消息打印出去。

                内核提供如下API用于注册和注销console:

                        void register_console(struct console *);

                        int unregister_console(struct console*);

                 在内核init/main.c文件中的start_kernel函数中,会调用console_init函数,该函数会调用位于内核存放console初始化函数的代码段,调用其中的每一个初始化console的函数。

                实际上,对于任何一个初始化console的函数而言,只需要通过console_initcall进行包装,即可把它放入.con_initcall.init节(开始地址为__com.initcall_start)。

                实际上,console_initcall是一个宏,其定义于include/linux/init.h文件,它可以展开成:

                        #define console_initcall(fn)  static initcall_t __initcall_##fn   __used __section(.con_initcall.init)=fn

                留意其中的__section(.con_initcall.init),实际上是一个链接阶段的指示,表明将指定的函数放入.con_initcall.init节。

                console_init()是由init/main.c文件中的start_kernel函数调用的,而在console_init被调用前,还执行了一系列的操作。为了在console_init被调用前就能使用printk(),可以使用内核的“early printk”支持,该选项位于内核配置菜单“Linux Kernel Configuration”下的“Kernel hacking”菜单之下。

                对于early printk的console的注册往往通过解析内核的early_param完成,如对于8250而言,定义了“earlycon”这样一个内核参数,当解析此内核参数时,相应地被early_param绑定的函数setup_early_serial8250_console被调用,此函数将注册一个用于early printk的console。

                这里在补充一个知识,内核的initcall分成了8级,对应的节分别为.initcall0.init、.initcall1.init、.initcall2.init、.initcall3.init、.initcall4.init、.initcall5.init、.initcall6.init、.initcall7.init,分别通过pure_initcall(fn)、con_initcall(fn)、postcore_initcall(fn)、arch_initcall(fn)、subsys_initcall(fn)、fs_initcall(fn)、device_initcall(fn)、late_initcall(fn)可将指定的函数放入对应的节。对于pure_initcall()而言,它指定的initcall不依赖于任何其他部分,因此,其指定函数只能built-in,不能在模块中。对于1~7级而言,还存在对应的sync版本,分别通过core_initcall_sync(fn)、postcore_initcall_sync(fn)、arch_initcall_sync(fn)、subsys_initcall_sync(fn)、fs_initcall_sync(fn)、device_initcall_sync(fn)、late_initcall_sync(fn)修饰。

        14.8 实例:S3C6410串口与console驱动

                14.8.1 S3C6410串口硬件描述

                14.8.2 S3C6410串口UART驱动

                14.8.3 S3C6410串口console驱动

15.Linux的I2C核型、总线与设备驱动

        15.1 Linux的I2C体系结构

                Linux的I2C体系结构分为3个组成部分。

                        (1)I2C核心

                                I2C核心提供了I2C总线驱动和设备驱动的注册、注销方法,I2C通信方法(即“algorithm”)上层的、与具体适配器无关的代码以及探测设备、检测设备地址的上层代码等。

                                Linux设备驱动实例

                        (2)I2C总线驱动

                                I2C总线驱动是对I2C硬件体系结构中适配器端的实现,适配器可由CPU控制,甚至可以直接集成在CPU内部。

                                I2C总线驱动主要包含了I2C适配器数据结构i2c_adapter、I2C适配器的algorithm数据结构i2c_alorithm和控制I2C适配器产生通信信号的函数。

                                经由I2C总线驱动的代码,我们可以控制I2C适配器以主控方式产生开始位、停止位、读写周期,以及以从设备方式被读写、产生ACK等。

                        (3)I2C设备驱动

                                I2C设备驱动(也称为客户驱动)是对I2C硬件体系结构中设备端的的实现,设备一般挂接在受CPU控制的I2C适配器上,通过I2C适配器与CPU交换数据。

                                I2C设备驱动主要包含了数据结构i2c_driver和i2c_client,我们需要根据具体设备实现其中的成员函数。

                                在Linux 2.6内核中,所有的I2C设备都在sysfs文件系统中显示,存于/sys/bus/i2c/目录,以适配器地址和芯片地址的形式列出。

                                在Linux内核源代码中的drivers目录下包含一个i2c目录,而在i2c目录下又包含如下文件夹。

                                        1)i2c-core.c

                                                这个文件实现了I2C核心的功能以及/proc/bus/i2c*接口。

                                        2)i2c-dev.c

                                                实现了I2C适配器设备文件的功能,每一个I2C适配器都被分配一个设备。通过适配器访问设备时的主设备号都为89,次设备号为0~255。应用程序通过“i2c-%d”(i2c-0,i2c-1,...,i2c-10,...)文件名并使用文件操作接口open()、write()、read()、ioctl()和close()等来访问这个设备。

                                               i2c-dev.c并没有针对特定的设备而设计,只是提供了通用的read()、write()和ioctl()等接口,应用层可以借用这些接口访问挂接在适配器上的I2C设备的存储空间或寄存器,并控制I2C设备的工作方式。

                                        3)chips文件夹

                                                这个目录中包含了一些特定的i2C设备驱动,如Dallas公司的DS1337实时钟芯片、EPSON公司的RTC8564实时钟芯片和I2C接口的EEPROM驱动等。

                                                在具体的I2C设备驱动中,调用的都是I2C核心提供的API,因此,这使得具体的I2C设备驱动不依赖于CPU的类型和I2C适配器的硬件特性。

                                        4)busses文件夹

                                                这个文件中包含了一些I2C总线的驱动,针对S3C2410、S3C2440和S3C6410等处理器的I2C控制器驱动为i2c-s3c2410.c。

                                        5)algos文件夹

                                                实现了一些I2C总线适配器的algorithm。

                                此外,内核中的i2c,h头文件对i2c_driver、i2c_client、i2c_adapter和i2c_algorithm这4个数据结构进行了定义。理解这4个结构体的作用十分关键。

                                下面分析i2c_driver、i2c_client、i2c_adapter和i2c_algorithm这4个数据结构的作用及其盘根错节的关系。

                                        1)i2c_adapter与i2c_algorithm

                                                 i2c_adapter对应于物理上的一个适配器,而i2c_alorithm对应一套通信方法。一个I2C适配器需要i2c_algorithm中提供的通信函数来控制适配器上产生特定的访问周期。缺少i2c_algorithm的i2c_adapter什么也做不了。因此i2c_adapter中包含其使用的i2c_algorithm的指针。

                                                 i2c_algorithm中的关键函数master_xfer()用于产生I2C访问周期需要的信号,以i2c_msg(即I2C消息)为单位。i2c_msg结构体也非常关键。

                                         2)i2c_driver与i2c_client

                                                 i2c_driver对应一套驱动方法,其主要成员函数是probe()、remove()、suspend()、resume()等,另外id_table是该驱动所支持的I2C