I/O体系结构

总线系统

  • PCI(Peripheral Component Interconnect)
  • ISA(Industrial Standard Architecture)
  • SBus
  • IEEE1394
  • USB(Universal Serial Bus)
  • SCSI(Small computer System Interface)
  • 并口与串口(Parallel and Serial Interface)

与外设的交互

I/O端口:处理器管理了一个独立的虚拟地址空间,可用于管理所有I/O地址. 每种处理器类型实现端口访问的方式都不同.因此,内核必须提供一个适当的抽象层.诸如outb等之类的命令在asm-arch/io.h中实现.

I/O内存映射:为使用内存映射,首先必须将I/O端口映射到普通的系统内存中.在不同的底层体系结构之上,完成这一任务的方法有很大的不同,内核再次提供了一个小的抽象层,主要包括ioremap和iounmap命令.

轮询与中断:中断是更好的备选方案.每个CPU都提供了中断线,可由各个系统设备共享.

通过总线控制设备

并非所有设备都是直接通过I/O语句寻址,也有通过总线系统访问的.

就系统总线而言(对很多处理器类型和体系结构来说,是PCI总线),可使用I/O语句和内存映射与总线自身和附接的设备通信。

扩展总线如USB、IEEE1394、SCSI等,通过明确定义的总线协议与附接的设备交换数据和命令。内核通过I/O语句和内存映射与总线自身通信,①同时提供了平台无关的例程,使总线能够与附接的设备通信。

访问设备

设备特殊文件用来访问扩展设备.这些文件并不关联到硬盘或其他任何存储介质上的数据段,而是建立了与某个设备驱动程序的连接,以支持与扩展设备的通信.

设备并不是通过其文件名标识,而是通过文件的主从设备号表示.这些号码在文件系统中作为特别的文件属性管理.

访问权限之前的字母是b或c,分别表示块设备和字符设备

设备文件没有文件长度,而增加了另外的两个值,分别是主设备号和从设备号.二者共同形成一个唯一的号码,内核可由此查找对应的设备驱动程序.

主设备号用于寻址设备驱动程序自身.驱动程序管理的各个设备则通过不同的从设备号指定.一个驱动程序也可以分配多个主设备号.

动态创建设备文件

几乎所有的发布版都将/dev内容的管理工作切换到udevd,这是一个守护进程,允许从用户层动态创建设备文件.

每当内核检查到一个设备时,都会创建一个内核对象kobject.该对象借助于sysfs文件系统导出到用户层.此外,内核还向用户空间发送一个热插拔消息,包含了驱动程序为设备分配的主从设备号.udevd守护进程所需完成的所有工作,就是监听这些消息.

主从设备号的表示

现在主从设备号一共32位,主设备号12个比特位(高),剩余的20个比特位用于从设备号(低).

为了与原来的16位兼容将比特范围0~7中的8个比特位用作从设备号的第一部分,接下来的12个比特位(比特范围8~19)用作主设备号,最后12个比特位用作从设备号剩余的部分.

这种划分的优点在于,该数据结构的前16个比特位,可以解释为旧的设备号。

内核提供了下列函数/宏(定义在<kdev_t.h>中),以便从u32表示提取信息,并在u32和dev_t 之间进行转换。

  • MAJOR和MINOR分别从dev_t提取主设备号和从设备号。
  • MKDEV(major, minor)根据主从设备号产生一个dev_t类型值
  • new_encode_dev将dev_t转换为具有上述外部表示的u32类型值
  • new_decode_dev将外部表示转换为dev_t。
  • old_encode_dev和old_decode_dev在旧的u16表示和现在的dev_t表示之间进行切换
<kdev_t.h>
u16 old_encode_dev(dev_t dev);
dev_t old_decode_dev(u16 val);
u32 new_encode_dev(dev_t dev);
dev_t new_decode_dev(u32 dev); 

注册

  • 每个字符设备都表示为struct cdev的一个实例

  • struct genhd用于管理块设备的分区,作用类似于字符设备的cdev.这是合理的,如果块设备没有分区,我们也可以视之为具有单一分区的块设备.

有两个全局数组(bdev_map用于块设备,cdev_map用于字符设备)用来实现散列表(关联设备号与设备),使用主设备号作为散列健.其都是struct kobj_map的实列

struct kobj_map {
    struct probe {
        struct probe *next;//将所有散列元素连接在一个单链表中
        dev_t dev;//表示设备号
        unsigned long range;//从设备号的连续范围
        struct module *owner;//指向提供设备驱动程序的模块
        kobj_probe_t *get;//指向一个函数,可以返回与设备关联的kobject实例
        int (*lock)(dev_t, void *);
        void *data;//对于字符设备,它指向struct cdev的一个实例,而对于块设备,则指向struct genhd的实例。
    } *probes[255];
    struct mutex *lock;
};

字符设备范围数据库

这里再次使用了散列表来跟踪已经分配的设备号范围,并同样使用主设备号作为散列键

static struct char_device_struct {
    struct char_device_struct *next;//连接同一散列行中的所有散列元素
    unsigned int major;//
    unsigned int baseminor;//baseminor是包含minorct个从设备号的连续范围中小的从设备号。
    int minorct;
    char name[64];
    struct file_operations *fops;//该设备关联的file_operations实例
    struct cdev *cdev;      /* will die */
} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

注册过程

字符设备

1.注册或分配一个设备号范围.如果驱动程序需要使用特定范围内的设备号,则必须调用register_chrdev_region,而alloc_chrdev_region则由内核来选择适当的范围(从chrdevs中分配)

<fs.h>

int register_chrdev_region(dev_t from, unsigned count, const char *name);//尽量不要使用该函数,因为对于大于255设备号无法工作

int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,const char *name); 

2.在获取了设备号范围后,需要将设备添加到字符设备数据库,以激活设备.这需要用cdev_init初始化一个struct cdev的实例,接下来调用cdev_add.(将其加入到cdev_map中,建立主从设备号与驱动的关联)

<cdev.h>

void cdev_init(struct cdev *cdev, const struct file_operations *fops);

int cdev_add(struct cdev *p, dev_t dev, unsigned count); 

块设备

注册块设备只需要调用add_disk一次.

较早的内核版本需要使用register_blkdev注册块设备,尽管现在不必再调用该函数,它 仍然是可用的。其好处在于,块设备将显示在/proc/devices.

与文件系统关联

<fs.h>
struct inode {
    ...
    dev_t      i_rdev;   //存储主从设备号
    ...
    umode_t      i_mode; //存储文件类型
    ...
    struct file_operations  *i_fop;
    ...
    union {
        ...
        struct block_device *i_bdev;
        struct cdev *i_cdev;

    };
    ...
}; 

标准文件操作

在打开一个设备文件时,各种文件系统的实现会调用init_special_inode函数,为块设备或字符设备文件创建一个inode.

fs/inode.c
void init_special_inode(struct inode *inode, umode_t mode, dev_t rdev)
{
    inode->i_mode = mode;
    if (S_ISCHR(mode)) {
        inode->i_fop = &def_chr_fops;
        inode->i_rdev = rdev;
    } else if (S_ISBLK(mode)) {
        inode->i_fop = &def_blk_fops;
        inode->i_rdev = rdev;
    } else
        printk(KERN_DEBUG "init_special_inode: bogus i_mode (%o)\n",mode);
}
 

用于字符设备的标准操作

字符设备的情况初非常含混,因为只有一个文件操作可用。

fs/devices.c
struct file_operations def_chr_fops = {
    .open = chrdev_open,
}; 

字符设备彼此非常不同。因而内核在开始不能提供多个操作,因为每个设备文件都需要一组独立、自定义的操作。因而chrdev_open函数的主要任务就是向该结构填入适用于已打开设备的函数指针,使得能够在设备文件上执行有意义的操作,并终能够操作设备自身

### 用于块设备的标准操作

相比字符设备,块设备遵循的方案更加一致。这使得内核刚开始就有很多操作可供选择。这些操作的指针群集到一个称作blk_fops的通用结构中。

fs/block_dev.c
const struct file_operations def_blk_fops = {
   .open    = blkdev_open,
   .release   = blkdev_close,
   .llseek    = block_llseek,
   .read    = do_sync_read,
   .write    = do_sync_write,
   .aio_read   = generic_file_aio_read,
   .aio_write   = generic_file_aio_write_nolock,
   .mmap    = generic_file_mmap,
   .fsync     = block_fsync,
   .unlocked_ioctl   = block_ioctl,
   .splice_read   = generic_file_splice_read,
   .splice_write   = generic_file_splice_write,
};

# 字符设备操作

### 表示字符设备

<cdev.h>
struct cdev {
   struct kobject kobj;
   struct module *owner;//指向驱动的模块
   const struct file_operations *ops;//实现了与硬件通信的具体操作
   struct list_head list;//链接表示该设备的所有设备文件的inode(inode中的i_devices用作链表元素)
   dev_t dev;//设备号
   unsigned int count;//表示与该设备关联的从设备号的数目
};

### 打开设备文件

chrdev_open用于打开字符设备的通用函数,然后调用struct file新的file_operations中的open方法,在设备上执行所需的初始化任务。该函数也可以对数据结构作一点修改,以适应特定的从设备号。

首先根据主设备号设置一个特定的文件操作集。其中包含的操作,接下来可以由根据从设备号选择的其他操作代替。

### 读写操作

读写字符设备文件的实际工作不是一项特别有趣的任务,因为虚拟文件和设备驱动程序代码之间已经建立了关联。调用标准库的读写函数,将向内核发出一些系统调用,最终调用file_operations结构中相关的操作。

# 块设备操作

块:是一个特定长度的字符序列,用于保存在内核和设备之间传输的数据。块的长度可以通过软件方式修改

扇区:是一个固定的硬件单位,指定了某个设备最少能够传输的数据量。

块不过是连续扇区的序列而已。内核将每个块设备都视为一个线性表,由按整数编号的扇区或块组成。

### 块设备的表示

裸块设备由struct block_device表示。

所有的块设备的inode都保存在伪文件系统bdev中,利用辅助函数bdget,给定dev_t表示的设备号,该函数通过查找伪文件系统,看对应的inode是否已经存在。

对块设备的读写请求不会立即执行对应的操作。相反,这些请求会汇总起来,经过协同之后传输到设备。因此,对应设备文件的file_operations结构中没有保存用于执行读写操作的具体函数。相反,其中包含了通用函数,如generic_read_file和generic_write_file。

### 数据结构

<linux\fs.h>
struct block_device {
   dev_t           bd_dev;  /* not a kdev_t - it's a search key */
   struct inode *      bd_inode;   /* will die *///指向bdev伪文件系统中表示该块设备的inode
   int         bd_openers;//统计用do_open打开该块设备的次数
   struct mutex        bd_mutex;   /* open/close mutex */
   struct semaphore    bd_mount_sem;
   struct list_head    bd_inodes;//包含表示该块设备的特殊设备文件的所有inode
   void *          bd_holder;//持有者,只有持有者能使用bd_private
   int         bd_holders;
#ifdef CONFIG_SYSFS
   struct list_head    bd_holder_list;
#endif
   struct block_device *   bd_contains;
   unsigned        bd_block_size;
   struct hd_struct *  bd_part;//指向一个专用的数据结构,表示包含在该块设备上的分区
   /* number of times partitions within this device have been opened. */
   unsigned        bd_part_count;//计算内核中引用该设备内分区的次数
   int         bd_invalidated;//表示分区在内核中的信息无效,因为磁盘上的分区已经改变。下一次打开该设备时,将要重新扫描分区表
   struct gendisk *    bd_disk;//提供另一个抽象层,也用来划分硬盘
   struct list_head    bd_list;//用于跟踪记录系统中所有可用的block_device实例。该链表的表头为全局变量all_bdevs。
   struct backing_dev_info *bd_inode_backing_dev_info;
   /*
    * Private data.  You must have bd_claim'ed the block_device
    * to use this.  NOTE:  bd_claim allows an owner to claim
    * the same device multiple times, the owner must take special
    * care to not mess up bd_private for that case.
    */
   unsigned long       bd_private;//私有数据存储
};

#### 通用硬盘和分区

硬盘上分区的信息不依赖于表示该分区的block_device实例。实际上,将一个硬盘添加到系统中,内核将读取并分析底层块设备上的分区信息,但并不会对各个分区创建block_decvice实例。

内核使用以下数据结构,对已经分区的硬盘提供了一种表示:

<linux\genhd.h>
struct gendisk {
   int major;          /* major number of driver *///设备驱动程序的主设备号
   int first_minor;
   int minors;                     /* maximum number of minors, =1 for
                                        * disks that can't be partitioned. *///从设备的最大数目,为1表示磁盘无分区
   char disk_name[32];     /* name of major driver *///给出磁盘名称
   struct hd_struct **part;    /* [indexed by minor] *///索引是从设备号
   int part_uevent_suppress;//如果设置正值,在检测到设备的分区信息改变时,就不会向用户空间 发送热插拔事件。
   struct block_device_operations *fops;//特定于设备,执行各种底层任务的各个函数
   struct request_queue *queue;//请求队列
   void *private_data;
   sector_t capacity;//磁盘容量,单位是扇区

   int flags;
   struct device *driverfs_dev;//标识磁盘所属的硬件设备
   struct kobject kobj;
   struct kobject *holder_dir;
   struct kobject *slave_dir;

   struct timer_rand_state *random;
   int policy;

   atomic_t sync_io;       /* RAID */
   unsigned long stamp;
   int in_flight;
#ifdef  CONFIG_SMP
   struct disk_stats *dkstats;
#else
   struct disk_stats dkstats;
#endif
   struct work_struct async_notify;
};

对于每个分区,都有一个hd_struct实例,用于描述该分区在设备内的键。

<linux\genhd.h>
struct hd_struct {
   sector_t start_sect;//该分区在块设备上的起始扇区
   sector_t nr_sects;//该分区在块设备上的长度
   struct kobject kobj;
   struct kobject *holder_dir;
   unsigned ios[2], sectors[2];    /* READs and WRITEs */
   int policy, partno;
#ifdef CONFIG_FAIL_MAKE_REQUEST
   int make_it_fail;
#endif
};

struct gendisk使用使用alloc_disk函数分配(del_gendisk释放)

#### 各个部分的联系

对于块设备上已经打开的每个分区,都对应于一个struct block_device的实例。对应于分区的block_device实例通过bd_contains关联到对应于整个设备的block_device实例。所有的block_device实例都通过bd_disk,指向通用硬盘数据结构gendisk。尽管一个已分区的磁盘有多个block_device实例,但只对应于一个gendisk实例。

gendisk实例中的part成员指向hd_struct指针的数组。每个数组项都表示一个分区。如果一个block_device表示分区,其中包含了一个指针指向所述的hd_struct,hd_struct实例在struct gendisk和 struct block_device之间共享。

此外,通用硬盘gendisk还集成到kobject框架中。块设备子系统由kset实例block_subsystem表示。kset中有一个链表,每个gendisk实例所包含的kobject实例都放置在该链表上。
由struct hd_struct表示的分区对象也包含了一个嵌入的kobject。概念上,分区是硬盘的子元素,这一点也被内核对象的数据结构所捕获。hd_struct中嵌入的kobject的parent指针,将指向通用硬盘gendisk中嵌入的kobject。

#### 块设备操作

Linux 块设备驱动代码编写:https://m.jb51.net/article/138227.htm

<fs.h>

struct block_device_operations {
    int (*open) (struct inode *, struct file *);//打开文件
    int (*release) (struct inode *, struct file *);//关闭文件
    int (*ioctl) (struct inode *, struct file *, unsigned, unsigned long);//向块设备发送特殊命令
    long (*unlocked_ioctl) (struct file *, unsigned, unsigned long);
    long (*compat_ioctl) (struct file *, unsigned, unsigned long);
    int (*direct_access) (struct block_device *, sector_t, unsigned long *);
    int (*media_changed) (struct gendisk *);//检查存储介质是否已经改变
    int (*revalidate_disk) (struct gendisk *);//让设备重新生效
    int (*getgeo)(struct block_device *, struct hd_geometry *);
    struct module *owner;
};

请求队列

<blkdev.h>

struct request_queue
{
    /*
     * Together with queue_head for cacheline sharing
     */
    struct list_head    queue_head;//是该数据结构的主要成员,是一个表头,用于构建I/O请求的双链表。链表每个元素的数据类型都是request,代表向块设备读取数据的一个请求
    struct request      *last_merge;
    elevator_t      *elevator;//电梯调度算法

    /*
     * the queue request freelist, one for reads and one for writes
     */
    struct request_list rq;//request实例的缓存

    request_fn_proc     *request_fn;//每个驱动程序必须亲自实现该函数,该函数时请求队列管理与各个设备的底层功能之间的主要联系
    make_request_fn     *make_request_fn;//创建新请求。内核对该函数的标准实现向请求链表添加请求
    prep_rq_fn      *prep_rq_fn;//用于在发送实际的请求之前预备一个请求。
    unplug_fn       *unplug_fn;//用于拔出一个块设备时调用
    merge_bvec_fn       *merge_bvec_fn;//确定是否允许向一个现存的请求增加更多数据。由于请求队列的长度通常是 固定的,限制了其中请求的数目,因此内核可使用这种机制来避免可能的问题。
    prepare_flush_fn    *prepare_flush_fn;//在预备刷出队列时,即一次性执行所有带决请求之前调用。在该方法中,设备可以进行必要的清理。
    softirq_done_fn     *softirq_done_fn;//对于大的请求来说,完成请求,即完成所有I/O,可能是一个耗时的过程。在内核版本2.6.16 开发期间,添加了使用软中断SoftIRQ(有关该机制的更多细节请参见第14章)异步完成请求 的特性。可以通过调用blk_complete_request要求异步完成请求,softirq_done_fn在这种情况下用作回调函数,通知驱动程序请求已经完成。

    /*
     * Dispatch queue sorting
     */
    sector_t        end_sector;
    struct request      *boundary_rq;

    /*
     * Auto-unplugging state
     */
    struct timer_list   unplug_timer;
    int         unplug_thresh;  /* After this many requests */
    unsigned long       unplug_delay;   /* After this many jiffies */
    struct work_struct  unplug_work;

    struct backing_dev_info backing_dev_info;

    /*
     * The queue owner gets to use this for whatever they like.
     * ll_rw_blk doesn't touch it.
     */
    void            *queuedata;

    /*
     * queue needs bounce pages for pages above this limit
     */
    unsigned long       bounce_pfn;
    gfp_t           bounce_gfp;

    /*
     * various queue flags, see QUEUE_* below
     */
    unsigned long       queue_flags;//控制队列的内部状态

    /*
     * protects queue structures from reentrancy. ->__queue_lock should
     * _never_ be used directly, it is queue private. always use
     * ->queue_lock.
     */
    spinlock_t      __queue_lock;
    spinlock_t      *queue_lock;

    /*
     * queue kobject
     */
    struct kobject kobj;

    /*
     * queue settings
     */
    unsigned long       nr_requests;    /* Max # of requests *///请求的最大数目
    unsigned int        nr_congestion_on;
    unsigned int        nr_congestion_off;
    unsigned int        nr_batching;

    unsigned int        max_sectors;//指定设备在单个请求中可以处理的扇区的最大数目。长度单位是具体设备的扇区长度
    unsigned int        max_hw_sectors;
    unsigned short      max_phys_segments;//指定用于运输不连续数据的分散—聚集请求中,不连续的段的大数目
    unsigned short      max_hw_segments;//与max_phys_segments相同,但考虑了(可能的)I/O MMU所进行的重新映射。
    unsigned short      hardsect_size;//指定了设备的物理扇区长度
    unsigned int        max_segment_size;//单个请求的最大段长度(按字节计算)

    unsigned long       seg_boundary_mask;
    unsigned int        dma_alignment;

    struct blk_queue_tag    *queue_tags;
    struct list_head    tag_busy_list;

    unsigned int        nr_sorted;
    unsigned int        in_flight;

    /*
     * sg stuff
     */
    unsigned int        sg_timeout;
    unsigned int        sg_reserved_size;
    int         node;
#ifdef CONFIG_BLK_DEV_IO_TRACE
    struct blk_trace    *blk_trace;
#endif
    /*
     * reserved for flush operations
     */
    unsigned int        ordered, next_ordered, ordseq;
    int         orderr, ordcolor;
    struct request      pre_flush_rq, bar_rq, post_flush_rq;
    struct request      *orig_bar_rq;

    struct mutex        sysfs_lock;

#if defined(CONFIG_BLK_DEV_BSG)
    struct bsg_class_device bsg_dev;
#endif
};

内核提供了标准函数blk_init_queue_node,用于产生一个标准的请求队列。在这种情况下,驱动程序自身唯一必须提供的管理函数就是request_fn。

向系统添加磁盘和分区

添加分区

add_partition负责向通用硬盘数据结构添加一个新的分区。

<fs/partitions/check.c>
void add_partition(struct gendisk *disk, int part, sector_t start, sector_t len, int flags)
{
    struct hd_struct *p;

    p = kzalloc(sizeof(*p), GFP_KERNEL);//分配一个新的hd_struct实例
    if (!p)
        return;

    p->start_sect = start;//信息填充
    p->nr_sects = len;
    p->partno = part;
    p->policy = disk->policy;

    if (isdigit(disk->kobj.k_name[strlen(disk->kobj.k_name)-1]))//指定用于显示的名字
        kobject_set_name(&p->kobj, "%sp%d",
                 kobject_name(&disk->kobj), part);
    else
        kobject_set_name(&p->kobj, "%s%d",
                 kobject_name(&disk->kobj),part);
    p->kobj.parent = &disk->kobj;//将分区内核对象的父对象设置为通用硬盘对象
    p->kobj.ktype = &ktype_part;
    kobject_init(&p->kobj);
    kobject_add(&p->kobj);//使之成为块设备子系统的一个成员
    if (!disk->part_uevent_suppress)
        kobject_uevent(&p->kobj, KOBJ_ADD);
    sysfs_create_link(&p->kobj, &block_subsys.kobj, "subsystem");
    if (flags & ADDPART_FLAG_WHOLEDISK) {
        static struct attribute addpartattr = {
            .name = "whole_disk",
            .mode = S_IRUSR | S_IRGRP | S_IROTH,
        };

        sysfs_create_file(&p->kobj, &addpartattr);
    }
    partition_sysfs_add_subdir(p);
    disk->part[part-1] = p;
}

添加磁盘

<block/genhd.c>

void add_disk(struct gendisk *disk)
{
    disk->flags |= GENHD_FL_UP;
    blk_register_region(MKDEV(disk->major, disk->first_minor),
                disk->minors, NULL, exact_match, exact_lock, disk);//在bdev_map中添加gendisk
    register_disk(disk);//获取该设备的一个新的block_device实例。也会调用rescan_partitions,扫描分区。并利用add_partition添加分区
    blk_register_queue(disk);
}

打开块设备文件

在用户应用程序打开一个块设备的设备文件时,虚拟文件系统代码调用file_operations结构的open函数,最终会调用到blkdev_open。

static int do_open(struct block_device *bdev, struct file *file, int for_part)
{
    struct module *owner = NULL;
    struct gendisk *disk;
    int ret = -ENXIO;
    int part;

    file->f_mapping = bdev->bd_inode->i_mapping;
    lock_kernel();
    disk = get_gendisk(bdev->bd_dev, &part);
    if (!disk) {
        unlock_kernel();
        bdput(bdev);
        return ret;
    }
    owner = disk->fops->owner;

    mutex_lock_nested(&bdev->bd_mutex, for_part);
    if (!bdev->bd_openers) {//设备此前未打开过
        bdev->bd_disk = disk;
        bdev->bd_contains = bdev;
        if (!part) {//主设备(该路径通常是通过add_disk走)
            struct backing_dev_info *bdi;
            if (disk->fops->open) {
                ret = disk->fops->open(bdev->bd_inode, file);
                if (ret)
                    goto out_first;
            }
            if (!bdev->bd_openers) {
                bd_set_size(bdev,(loff_t)get_capacity(disk)<<9);
                bdi = blk_get_backing_dev_info(bdev);
                if (bdi == NULL)
                    bdi = &default_backing_dev_info;
                bdev->bd_inode->i_data.backing_dev_info = bdi;
            }
            if (bdev->bd_invalidated)//现存分区无效,重新读取分区表
                rescan_partitions(disk, bdev);
        } else {//如果打开的块设备表示一个此前没有打开过的分区,内核需要将分区的block_device实例与包含分区的block_device关联起来
            struct hd_struct *p;
            struct block_device *whole;
            whole = bdget_disk(disk, 0);
            ret = -ENOMEM;
            if (!whole)
                goto out_first;
            BUG_ON(for_part);
            ret = __blkdev_get(whole, file->f_mode, file->f_flags, 1);
            if (ret)
                goto out_first;
            bdev->bd_contains = whole;
            p = disk->part[part - 1];
            bdev->bd_inode->i_data.backing_dev_info =
               whole->bd_inode->i_data.backing_dev_info;
            if (!(disk->flags & GENHD_FL_UP) || !p || !p->nr_sects) {
                ret = -ENXIO;
                goto out_first;
            }
            kobject_get(&p->kobj);
            bdev->bd_part = p;
            bd_set_size(bdev, (loff_t) p->nr_sects << 9);
        }
    } else {
        put_disk(disk);
        module_put(owner);
        if (bdev->bd_contains == bdev) {
            if (bdev->bd_disk->fops->open) {
                ret = bdev->bd_disk->fops->open(bdev->bd_inode, file);
                if (ret)
                    goto out;
            }
            if (bdev->bd_invalidated)
                rescan_partitions(bdev->bd_disk, bdev);
        }
    }
    bdev->bd_openers++;
    if (for_part)
        bdev->bd_part_count++;
    mutex_unlock(&bdev->bd_mutex);
    unlock_kernel();
    return 0;

out_first:
    bdev->bd_disk = NULL;
    bdev->bd_inode->i_data.backing_dev_info = &default_backing_dev_info;
    if (bdev != bdev->bd_contains)
        __blkdev_put(bdev->bd_contains, 1);
    bdev->bd_contains = NULL;
    put_disk(disk);
    module_put(owner);
out:
    mutex_unlock(&bdev->bd_mutex);
    unlock_kernel();
    if (ret)
        bdput(bdev);
    return ret;
}

请求结构

内核提供了数据结构以描述发送给块设备的请求

<blkdev.h>
struct request {
    struct list_head queuelist;//连接到请求队列
    struct list_head donelist;//连接到完成链表

    struct request_queue *q;//指向请求所属的请求队列

    //请求可用于向设备传送控制命令
    unsigned int cmd_flags;//_REQ_RW特别重要,因为它指出了数据传输的方向。如果该比特位置位,则将数据写入设备; 否则,从设备读取数据
    enum rq_cmd_type_bits cmd_type;//通常为REQ_TYPE_FS:表示文件系统请求

    /* Maintain bio traversal state for part by part I/O submission.
     * hard_* are block layer internals, no driver should touch them!
     */
    //hard_sector、hard_cur_sectors和hard_nr_sectors与结构中没有hard_前缀的对应成员语 义相同,但涉及的是实际硬件而非虚拟设备。通常两组变量的值相同,但在使用RAID或逻辑卷管理 器(Logical Volume Manager)时可能会有差别,因为这些机制实际上是将几个物理设备合并为一个虚 拟设备。
    sector_t sector;        /* next sector to submit *///需要传输的下一个扇区号
    sector_t hard_sector;       /* next sector to complete *///需要传输的下一个扇区号
    unsigned long nr_sectors;   /* no. of sectors left to submit *///还需要传输的扇区数目
    unsigned long hard_nr_sectors;  /* no. of sectors left to complete *///还需要传输的扇区数目
    /* no. of sectors left to submit in the current segment */
    unsigned int current_nr_sectors;//当前段中还需要传输的扇区数目

    /* no. of sectors left to complete in the current segment */
    unsigned int hard_cur_sectors;//当前段中还需要传输的扇区数目

    //bio用于在系统和设备之间传输数据
    struct bio *bio;//标识传输尚未完成的当前BIO实例
    struct bio *biotail;//指向最后一个BIO实例,因为一个请求中可使用多个BIO

    struct hlist_node hash; /* merge hash */
    /*
     * The rb_node is only used inside the io scheduler, requests
     * are pruned when moved to the dispatch queue. So let the
     * completion_data share space with the rb_node.
     */
    union {
        struct rb_node rb_node; /* sort/lookup */
        void *completion_data;
    };

    /*
     * two pointers are available for the IO schedulers, if they need
     * more they have to dynamically allocate it.
     */
    void *elevator_private;
    void *elevator_private2;

    struct gendisk *rq_disk;
    unsigned long start_time;

    /* Number of scatter-gather DMA addr+len pairs after
     * physical address coalescing is performed.
     */
    unsigned short nr_phys_segments;

    /* Number of scatter-gather addr+len pairs after
     * physical and DMA remapping hardware coalescing is performed.
     * This is the number of scatter-gather entries the driver
     * will actually have to deal with after DMA mapping is done.
     */
    unsigned short nr_hw_segments;

    unsigned short ioprio;

    void *special;
    char *buffer;

    int tag;
    int errors;

    int ref_count;

    /*
     * when request is used as a packet command carrier
     */
    unsigned int cmd_len;
    unsigned char cmd[BLK_MAX_CDB];

    unsigned int data_len;
    unsigned int sense_len;
    void *data;
    void *sense;

    unsigned int timeout;
    int retries;

    /*
     * completion callback.
     */
    rq_end_io_fn *end_io;
    void *end_io_data;

    /* for bidi */
    struct request *next_rq;
};

BIO

struct bio {
    sector_t        bi_sector;  /* device address in 512 byte//指定了传输开始的扇区号
                           sectors */
    struct bio      *bi_next;   /* request queue link *///将与请求关联的几个bio组织到一个单链表中
    struct block_device *bi_bdev;//指向请求所属设备的block_device数据结构。
    unsigned long       bi_flags;   /* status, command, etc */
    unsigned long       bi_rw;      /* bottom bits READ/WRITE,
                         * top bits priority
                         */

    unsigned short      bi_vcnt;    /* how many bio_vec's *///bio_vec的数目
    unsigned short      bi_idx;     /* current index into bvl_vec *///bi_io_vec数组中,当前处理数组项的索引

    /* Number of segments in this BIO after
     * physical address coalescing is performed.
     */
    unsigned short      bi_phys_segments;

    /* Number of segments after physical and DMA remapping
     * hardware coalescing is performed.
     */
    unsigned short      bi_hw_segments;

    unsigned int        bi_size;    /* residual I/O count *///表示请求所涉及数据的长度,单位为字节

    /*
     * To keep track of the max hw size, we account for the
     * sizes of the first and last virtually mergeable segments
     * in this bio
     */
    unsigned int        bi_hw_front_size;
    unsigned int        bi_hw_back_size;

    unsigned int        bi_max_vecs;    /* max bvl_vecs we can hold */

    struct bio_vec      *bi_io_vec; /* the actual vec list *///实际的bio_vec数组

    bio_end_io_t        *bi_end_io;//在硬件传输完成时,设备驱动程序必须调用bi_end_io。这使得块设备层有机会进行清理,或唤醒等待该清理结束的睡眠进程
    atomic_t        bi_cnt;     /* pin count */

    void            *bi_private;//私有数据

    bio_destructor_t    *bi_destructor; /* destructor *///指向一个析构函数,在从内存删除一个bio实例之前调用
};

提交请求

内核分两个步骤提交请求。

  • 它首先创建一个bio实例以描述请求,然后将该实例嵌入到请求中,并置于请求队列上。
  • 接下来内核将处理请求队列并执行bio中的操作。

创建请求

submit_bio是一个关键函数,负责根据传递的bio实例创建一个新请求,并使用make_request_fn将请求置于驱动程序的请求队列上。

submit_bio->generic_make_request:该函数主要是处理递归调用generic_make_request(p347)

generic_make_request->__generic_make_request:

  • 使用bdev_get_queue,找到该请求所涉及块设备的请求队列
  • 如果该设备是分区的,则用blk_partition_remap重新映射该请求,以确保读写正确的区域。
  • q->make_request_fn根据bio产生请求并发送给设备驱动程序(默认实现__make_request)
<block/ll_rw_blk.c>
static int __make_request(struct request_queue *q, struct bio *bio)
{
    struct request *req;
    int el_ret, nr_sectors, barrier, err;
    const unsigned short prio = bio_prio(bio);
    const int sync = bio_sync(bio);
    int rw_flags;

    nr_sectors = bio_sectors(bio);

    /*
     * low level driver can indicate that it wants pages above a
     * certain limit bounced to low memory (ie for highmem, or even
     * ISA dma in theory)
     */
    blk_queue_bounce(q, &bio);

    barrier = bio_barrier(bio);
    if (unlikely(barrier) && (q->next_ordered == QUEUE_ORDERED_NONE)) {
        err = -EOPNOTSUPP;
        goto end_io;
    }

    spin_lock_irq(q->queue_lock);

    if (unlikely(barrier) || elv_queue_empty(q))队列为空
        goto get_rq;

    el_ret = elv_merge(q, &req, bio);//查看新请求能否和现存请求合并
    switch (el_ret) {
        case ELEVATOR_BACK_MERGE:
            BUG_ON(!rq_mergeable(req));

            if (!ll_back_merge_fn(q, req, bio))
                break;

            blk_add_trace_bio(q, bio, BLK_TA_BACKMERGE);

            req->biotail->bi_next = bio;
            req->biotail = bio;
            req->nr_sectors = req->hard_nr_sectors += nr_sectors;
            req->ioprio = ioprio_best(req->ioprio, prio);
            drive_stat_acct(req, 0);
            if (!attempt_back_merge(q, req))
                elv_merged_request(q, req, el_ret);
            goto out;

        case ELEVATOR_FRONT_MERGE:
            BUG_ON(!rq_mergeable(req));

            if (!ll_front_merge_fn(q, req, bio))
                break;

            blk_add_trace_bio(q, bio, BLK_TA_FRONTMERGE);

            bio->bi_next = req->bio;
            req->bio = bio;

            /*
             * may not be valid. if the low level driver said
             * it didn't need a bounce buffer then it better
             * not touch req->buffer either...
             */
            req->buffer = bio_data(bio);
            req->current_nr_sectors = bio_cur_sectors(bio);
            req->hard_cur_sectors = req->current_nr_sectors;
            req->sector = req->hard_sector = bio->bi_sector;
            req->nr_sectors = req->hard_nr_sectors += nr_sectors;
            req->ioprio = ioprio_best(req->ioprio, prio);
            drive_stat_acct(req, 0);
            if (!attempt_front_merge(q, req))
                elv_merged_request(q, req, el_ret);
            goto out;

        /* ELV_NO_MERGE: elevator says don't/can't merge. */
        default:
            ;
    }

get_rq://将新请求添加到请求队列中
    /*
     * This sync check and mask will be re-done in init_request_from_bio(),
     * but we need to set it earlier to expose the sync flag to the
     * rq allocator and io schedulers.
     */
    rw_flags = bio_data_dir(bio);
    if (sync)
        rw_flags |= REQ_RW_SYNC;

    /*
     * Grab a free request. This is might sleep but can not fail.
     * Returns with the queue unlocked.
     */
    req = get_request_wait(q, rw_flags, bio);//分配一个新请求

    /*
     * After dropping the lock and possibly sleeping here, our request
     * may now be mergeable after it had proven unmergeable (above).
     * We don't worry about that case for efficiency. It won't happen
     * often, and the elevators are able to handle it.
     */
    init_request_from_bio(req, bio);//将bio中的数据 填到请求实例中

    spin_lock_irq(q->queue_lock);
    if (elv_queue_empty(q))
        blk_plug_device(q);//队列插入,等待处理
    add_request(q, req);
out:
    if (sync)//该请求需要同步处理
        __generic_unplug_device(q);//拔出队列,立即处理

    spin_unlock_irq(q->queue_lock);
    return 0;

end_io:
    bio_endio(bio, err);
    return 0;
}

队列插入

内核使用队列插入机制,来有意的阻止请求的处理(汇集一些请求,然后合并,提高性能)。请求队列可能处于空闲状态或者插入状态。如果队列处于空闲状态,队列中等待的请求将会被处理。否则,新的请求只是添加到队列,但并不处理。如果队列处于插入状态,则request_queue的queue_flags成员中QUEUE_ FLAG_PLUGGED标志置位。内核提供了blk_queue_plugged辅助函数检查该标志。

void blk_plug_device(struct request_queue *q)
{
    WARN_ON(!irqs_disabled());

    /*
     * don't plug a stopped queue, it must be paired with blk_start_queue()
     * which will restart the queueing
     */
    if (blk_queue_stopped(q))
        return;

    if (!test_and_set_bit(QUEUE_FLAG_PLUGGED, &q->queue_flags)) {//如果队列不为插入状态,则设置
        mod_timer(&q->unplug_timer, jiffies + q->unplug_delay);//设置拔出定时器(会调用blk_unplug_timeout拔出队列)
        blk_add_trace_generic(q, NULL, 0, BLK_TA_PLUG);
    }
}

还有另一个机制可用于拔出队列。如果当前读写请求的数目(保存在请求链表的count数组的两个数组项中)达到unplug_thresh指定的阈值,则elv_insert中调用__generic_unplug_device以触发拔出操作,使得等待的请求得到处理。

void __generic_unplug_device(struct request_queue *q)
{
    if (unlikely(blk_queue_stopped(q)))
        return;

    if (!blk_remove_plug(q))//清除队列的插入状态和用于自动拔出的定时器
        return;

    q->request_fn(q);//处理等待的请求
}

执行请求

在请求队列中的请求即将处理时,会调用特定于设备的request_fn函数。该任务与硬件的关联非常紧密,因此内核不会提供默认的实现。

simple_request是一个与硬件无关的示例,用于说明所有驱动程序在request_fn中所执行的基本步骤.

void sample_request (request_queue_t *q)
{
    int status;
    struct request *req;

    while ((req = elv_next_request(q)) != NULL){
        if (!blk_fs_request(req))    //判断req是否是诊断信息
            end_request(req, 0);    //标准的内核函数,用于从请求队列删除请求,还调用bio的bi_end_io函数
            continue;

        status = perform_sample_transfer(req);
        end_request(req, status);
    }
}

I/O调度

struct elevator_ops
{
    elevator_merge_fn *elevator_merge_fn;//检查一个新的请求是否可以与现存请求合并它还指定了请求插入到请求队列中的位置。
    elevator_merged_fn *elevator_merged_fn;//在两个请求已经合并后调用
    elevator_merge_req_fn *elevator_merge_req_fn;//将两个请求合并为一个请求。
    elevator_allow_merge_fn *elevator_allow_merge_fn;

    elevator_dispatch_fn *elevator_dispatch_fn;//从给定的请求队列中选择下一步应该调度执行的请求。
    elevator_add_req_fn *elevator_add_req_fn;//向请求队列添加请求
    elevator_activate_req_fn *elevator_activate_req_fn;
    elevator_deactivate_req_fn *elevator_deactivate_req_fn;

    elevator_queue_empty_fn *elevator_queue_empty_fn;//检查队列是否包含可供处理的请求。
    elevator_completed_req_fn *elevator_completed_req_fn;

    elevator_request_list_fn *elevator_former_req_fn;//分别查找给定请求的前一个和后一个请求
    elevator_request_list_fn *elevator_latter_req_fn;

    elevator_set_req_fn *elevator_set_req_fn;//在创建新请求和释放回内存管理子系统时调用
    elevator_put_req_fn *elevator_put_req_fn;

    elevator_may_queue_fn *elevator_may_queue_fn;

    elevator_init_fn *elevator_init_fn;//分别在队列初始化和释放时调用
    elevator_exit_fn *elevator_exit_fn;
    void (*trim)(struct io_context *);
};

I/O调度器不仅负责请求重排,还负责请求队列全部的管理工作。

每个I/O调度器都封装在下列数据结构中,其中包含了供内核使用的其他管理信息.

struct elevator_type
{
    struct list_head list;//内核将所有IO调度器在一个标准的双链表中维护(表头为elv_list)
    struct elevator_ops ops;
    struct elv_fs_entry *elevator_attrs;
    char elevator_name[ELV_NAME_MAX];
    struct module *elevator_owner;
};

资源分配

资源管理

struct resource {
    resource_size_t start;//指一般性的区域
    resource_size_t end;
    const char *name;//资源名称
    unsigned long flags;//准确地描述资源及其当前状态
    struct resource *parent, *sibling, *child;//建立树型层次结构
};

连接parent、child和sibling成员的规则很简单

  • 每个子结点只有一个父结点
  • 一个父结点可以有任意数目的子结点
  • 同一个父结点的所有子结点,会连接到兄弟结点链表上。

子结点可将区域划分为越来越小、功能越来越具体的部分。

请求和释放资源

请求和释放资源,无非是从资源树中添加和删除项而已。

kernel/resource.c

int request_resource(struct resource *root, struct resource *new);

int release_resource(struct resource *old);

I/O内存

I/O内存不仅包括与扩展设备通信直接使用的内存区域,还包括系统中可用的物理内存和ROM存储器,以及包含在资源列表中的内存。

可以通过 cat /proc/iomem 查看

所有分配的I/O内存地址,都通过一颗资源数管理,树的根结点是全局内核变量iomem_resource。

I/O端口

kernel/resource.c中的ioport_resource充当资源树的根结点。proc文件系统中的ioports文件可以显示已经分配的端口地址。

系统总线

通用驱动程序模型

所有总线共有的属性封装到特殊的、可以通用方法处理的数据结构中,再关联到总线相关的成员。

通用驱动程序模型主要基于通用对象模型。

设备的表示

驱动程序模型采用一种特殊数据结构来表示几乎所有总线类型通用的设备属性。该结构直接嵌入到特定于总线的数据结构中,而不是通过指针引用,这与前文介绍的kobject相似。

<device.h>

struct device {
    struct klist        klist_children;     //链表用于建立设备之间的层次关系
    struct klist_node   knode_parent;       /* node in sibling list *///兄弟结点链表中的结点
    struct klist_node   knode_driver;       //因为一个设备驱动程序能够服务多个设备,该链表元素用于将这些设备连接起来
    struct klist_node   knode_bus;
    struct device       *parent;

    struct kobject kobj;
    char    bus_id[BUS_ID_SIZE];    /* position on parent bus *///在宿主总线上的位置,设备在PCI总线上的位置由一个具有以下格式的字符串唯一地定义:<总线编号>:<插槽编号>.<功能编号>。
    struct device_type  *type;
    unsigned        is_registered:1;
    unsigned        uevent_suppress:1;

    struct semaphore    sem;    /* semaphore to synchronize calls to
                     * its driver.
                     */

    struct bus_type * bus;      /* type of bus device is on *///指向该设备所在总线的数据结构的实例。
    struct device_driver *driver;   /* which driver has allocated this//分配当前device实例的驱动程序
                       device */
    void        *driver_data;   /* data private to the driver *///驱动程序的私有数据
    void        *platform_data; /* Platform specific data, device//特定于平台的数据,设备模型的代码不会访问
                       core doesn't touch it */
    struct dev_pm_info  power;

#ifdef CONFIG_NUMA
    int     numa_node;  /* NUMA node this device is close to */
#endif
    u64     *dma_mask;  /* dma mask (if dma'able device) */
    u64     coherent_dma_mask;/* Like dma_mask, but for
                         alloc_coherent mappings as
                         not all hardware supports
                         64 bit addresses for consistent
                         allocations such descriptors. */

    struct list_head    dma_pools;  /* dma pools (if dma'ble) */

    struct dma_coherent_mem *dma_mem; /* internal for coherent mem
                         override */
    /* arch specific additions */
    struct dev_archdata archdata;

    spinlock_t      devres_lock;
    struct list_head    devres_head;

    /* class_device migration path */
    struct list_head    node;
    struct class        *class;
    dev_t           devt;       /* dev_t, creates the sysfs "dev" */
    struct attribute_group  **groups;   /* optional groups */

    void    (*release)(struct device * dev);//析构函数
};

内核提供了标准函数device_register,用于将一个新设备添加到内核的数据结构。

通用驱动程序模型也为设备驱动程序单独设计了一种数据结构。

<driver.h>

struct device_driver {
    const char      * name;//标识该驱动程序
    struct bus_type     * bus;//表示总线对象

    struct kobject      kobj;
    struct klist        klist_devices;//连接该驱动程序控制的所有设备
    struct klist_node   knode_bus;//用于连接一条公共总线上的所有设备

    struct module       * owner;
    const char      * mod_name; /* used for built-in modules */
    struct module_kobject   * mkobj;

    int (*probe)    (struct device * dev);//用于检查系统中是否存中能够用该设备驱动程序处理的设备
    int (*remove)   (struct device * dev);//删除系统中的设备时会调用
    void    (*shutdown) (struct device * dev);//一下三个函数分别用于电源管理
    int (*suspend)  (struct device * dev, pm_message_t state);
    int (*resume)   (struct device * dev);
};

驱动程序使用内核的标准函数driver_register注册到系统中,该函数在下文讨论。

总线的表示

<device.h>
struct bus_type {
    const char      * name;//总线名称
    struct module       * owner;

    struct kset     subsys;//提供了与总线子系统的关联。对应的总线出现在/sys/bus/busname
    struct kset     drivers;//与总线关联的所有设备和驱动程序(kset保证了与sysfs文件系统的自动集成)
    struct kset     devices;
    struct klist        klist_devices;
    struct klist        klist_drivers;

    struct blocking_notifier_head bus_notifier;

    struct bus_attribute    * bus_attrs;
    struct device_attribute * dev_attrs;
    struct driver_attribute * drv_attrs;

    int     (*match)(struct device * dev, struct device_driver * drv);//查找给定设备匹配的驱动程序
    int     (*uevent)(struct device *dev, struct kobj_uevent_env *env);
    int     (*probe)(struct device * dev);//检查设备在系统中是否真正存中
    int     (*remove)(struct device * dev);//删除驱动程序与设备之间的关联
    void        (*shutdown)(struct device * dev);//下面函数用于电源管理

    int (*suspend)(struct device * dev, pm_message_t state);
    int (*suspend_late)(struct device * dev, pm_message_t state);
    int (*resume_early)(struct device * dev);
    int (*resume)(struct device * dev);

    unsigned int drivers_autoprobe:1;
};

注册过程

在可以注册设备及其驱动程序之前,需要有总线。

bus.c

int bus_register(struct bus_type * bus)
{
    int retval;

    BLOCKING_INIT_NOTIFIER_HEAD(&bus->bus_notifier);

    retval = kobject_set_name(&bus->subsys.kobj, "%s", bus->name);
    if (retval)
        goto out;

    bus->subsys.kobj.kset = &bus_subsys;

    retval = subsystem_register(&bus->subsys);//通过嵌入的kset类型成员subsys,将新总线添加到总线子系统
    if (retval)
        goto out;

    retval = bus_create_file(bus, &bus_attr_uevent);
    if (retval)
        goto bus_uevent_fail;

    kobject_set_name(&bus->devices.kobj, "devices");
    bus->devices.kobj.parent = &bus->subsys.kobj;
    retval = kset_register(&bus->devices);//总线中的设备
    if (retval)
        goto bus_devices_fail;

    kobject_set_name(&bus->drivers.kobj, "drivers");
    bus->drivers.kobj.parent = &bus->subsys.kobj;
    bus->drivers.ktype = &driver_ktype;
    retval = kset_register(&bus->drivers);//总线中的驱动
    if (retval)
        goto bus_drivers_fail;

    klist_init(&bus->klist_devices, klist_devices_get, klist_devices_put);
    klist_init(&bus->klist_drivers, NULL, NULL);

    bus->drivers_autoprobe = 1;
    retval = add_probe_files(bus);
    if (retval)
        goto bus_probe_files_fail;

    retval = bus_add_attrs(bus);
    if (retval)
        goto bus_attrs_fail;

    pr_debug("bus type '%s' registered\n", bus->name);
    return 0;

bus_attrs_fail:
    remove_probe_files(bus);
bus_probe_files_fail:
    kset_unregister(&bus->drivers);
bus_drivers_fail:
    kset_unregister(&bus->devices);
bus_devices_fail:
    bus_remove_file(bus, &bus_attr_uevent);
bus_uevent_fail:
    subsystem_unregister(&bus->subsys);
out:
    return retval;
}

注册设备包括两个独立的步骤:初始化设备的数据结构,并将其加入到数据结构的网络中。

device_register用于注册设备。

device_register->device_initialize:主要通过kobj_set_kset_s(dev,devices_subsys)将设备添加到设备子系统

device_register->device_add:

  • 首先,将通过device->parent指定的父子关系转变为一般的内核对象层次结构
  • 在设备子系统中注册该设备
  • 然后调用bus_add_device在sysfs中添加两个链接:一个在总线目录下指向设备,另一个在设备的目录下指向总线系统
  • 调用bus_attach_device自动探测设备,查找适当的驱动程序,如果找到,则将设备添加到bus->klist_devices中

在进行一些检查和初始化工作之后,driver_register调用bus_add_driver将一个新驱动程序添加到一个总线。同样,驱动程序首先要有名字,然后注册到通用数据结构的框架中。

如果总线支持自动探测,则调用driver_attach。该函数迭代总线上的所有设备,使用驱动程序的match函数进行检测,确定是否有某些设备可使用该驱动程序管理。最后,将该设备驱动程序添加到总线上注册的所有驱动程序的链表中。

PCI总线(peripheral component interconnect bus)

PCI总线规定了以下设计目标。

  • 支持高传输带宽,以适应具有大数据流的多媒体应用。
  • 简单且易于自动化配置附接的外设
  • 平台独立性,即不绑定到特定的处理器或系统平台

PCI系统的布局

设备标识

系统的某个PCI总线上的每个设备,都由一组3个编号标识。

  • 总线编号:该设备所在总线编号,编号照例从0开始。PCI规范准许每个系统最多255个总线。
  • 插槽编号:总线内部的一个唯一标识编号。一个总线最多能附接32个设备。不同总线上的设备插槽编号可能相同
  • 功能编号:用于在一个扩展卡上,实现包括多个扩展设备的设备。最大数目为8

每个设备都通过一个16位编号唯一地标识,其中8个比特位用于总线编号,5个比特位用于插槽编号,3个比特位用于功能编号。

地址空间

有3个地址空间支持与PCI设备的通信

  • IO空间通过32个比特位描述,因而,对用于与设备通信的端口地址,提供了最大4GB的空间。
  • 取决于处理器类型,数据空间由32或64比特位描述
  • 配置空间包含了各个设备的类型和特征的详细信息,以省去危险的自动探测工作。

这些地址空间会根据处理器映射到系统虚拟内存中的不同位置,使得内核和设备驱动程序能够访问对应的资源。

配置信息

每个PCI设备都有一个256字节长的配置空间,其中包括了该设备的特点和要求的有关信息。

Linux内核入门到放弃-设备驱动程序-《深入Linux内核架构》笔记-LMLPHP

Vendor ID 和 Device ID 唯一地标识了厂商和设备类,。两个具有相似名称的附加字段:Subsystem Vendor ID 和Subsystem Device ID,也可以同时使用,以更精确地描述设备的通用接口。Rev ID用于区分不同的设备修订级别。

Class Code字段用于将设备分配到各种不同的功能组,该字段分为两部分。前8个比特位表示基类 (base class),而剩余的16个比特位表示基类的一个子类。基类及其子类的例子如下给:

  • 大容量存储器(PCI_BASE_CLASS_STORAGE)
  1. SCSI控制器(PCI_CLASS_STORAGE_SCSI)
  2. IDE控制器(PCI_CLASS_STORAGE_IDE)
  3. RAID控制器(PCI_CLASS_STORAGE_RAID)(用于组合多个磁盘驱动器)
  • 网络(PCI_BASE_CLASS_NETWORK)
  1. 以太网(PCI_BASE_NETWORK_ETHERNET)
  2. FDDI(PCI_BASE_NETWORK_FDDI)
  • 系统组件(PCI_BASE_CLASS_SYSTEM)
  1. DMA控制器(PCI_CLASS_SYSTEM_DMA)
  2. 实时时钟(PCI_CLASS_SYSTEM_RTC)

6个基地址字段,用于定义PCI设备和系统其余部分通信所用的地址。

就内核而言,剩余字段中相关的只有IRQ编号,可以接受0和255 之间的任意值,用于指定设备使用的中断。值为0表示该设备并不使用中断。

内核中的实现

数据结构

  • 系统中的各个总线由pci_bus的实例表示
  • pci_dev结构表示各个设备,扩展卡和功能部件
  • 每个驱动程序都通过pci_driver的一个实例描述

内核定义了两个全局的list_head变量,用作PCI数据结构形成的网络入口。

  • pci_root_buses列出了系统中所有的PCI总线。在“向下”扫描数据结构以查找附接到各个总线的所有设备时,该链表是一个起点。
  • pci_devices将系统中的所有PCI设备都链接起来,不考虑总线结构的影响。在驱动程序想要搜索它支持的所有设备时,该链表很有用
总线的表示
struct pci_bus {
    struct list_head node;      /* node in list of buses *///总线链表中的结点(pci_root_buses)
    struct pci_bus  *parent;    /* parent bus this bridge is on *///总线所在的父总线
    struct list_head children;  /* list of child buses *///子总线列表
    struct list_head devices;   /* list of devices on this bus *///总线上设备的链表
    struct pci_dev  *self;      /* bridge device as seen by parent *///父总线所看到的桥接器设备(除总线0以外,所有系统总线都可以只通过一个PCI桥接器寻址,桥接器类似于一个普通的PCI设备)
    struct resource *resource[PCI_BUS_NUM_RESOURCES];//导向到该总线的地址空间
                    /* address space routed to this bus */

    struct pci_ops  *ops;       /* configuration access functions *///访问配置信息的各函数
    void        *sysdata;   /* hook for sys-specific extension *///用于特定硬件的扩展
    struct proc_dir_entry *procdir; /* directory entry in /proc/bus/pci */// proc/bus/pci中的目录项

    unsigned char   number;     /* bus number *///总线号
    unsigned char   primary;    /* number of primary bridge *///主桥接器编号
    unsigned char   secondary;  /* number of secondary bridge *///次桥接器编号
    unsigned char   subordinate;    /* max number of subordinate buses *///下级总线的最大数目

    char        name[48];

    unsigned short  bridge_ctl; /* manage NO_ISA/FBB/et al behaviors */
    pci_bus_flags_t bus_flags;  /* Inherited by child busses */
    struct device       *bridge;
    struct class_device class_dev;
    struct bin_attribute    *legacy_io; /* legacy I/O for this bus */
    struct bin_attribute    *legacy_mem; /* legacy mem */
};
设备管理
<pci.h>

struct pci_dev {
    struct list_head global_list;   /* node in list of all PCI devices */   //在所有PCI设备的链表中的结点
    struct list_head bus_list;  /* node in per-bus list */  //在各总线设备链表中的结点
    struct pci_bus  *bus;       /* bus this device is on */ //设备所在的总线
    struct pci_bus  *subordinate;   /* bus this device bridges to */    //该桥接器设备连通的总线(指向“下级”总线)

    void        *sysdata;   /* hook for sys-specific extension */
    struct proc_dir_entry *procent; /* device entry in /proc/bus/pci */

    //devfn和 rom_base_reg之间的所有成员只是用于存储上文提到的配置空间数据。
    unsigned int    devfn;      /* encoded device & function index */   //编码过的设备和功能索引
    unsigned short  vendor;
    unsigned short  device;
    unsigned short  subsystem_vendor;
    unsigned short  subsystem_device;
    unsigned int    class;      /* 3 bytes: (base,sub,prog-if) */   //3个字节(base、sub、prog-if)
    u8      revision;   /* PCI revision, low byte of class word */  /* PCI修订版本号,class的低字节 */
    u8      hdr_type;   /* PCI header type (`multi' flag masked out) */ /* PCI首部类型(屏蔽了多个标志) */
    u8      pcie_type;  /* PCI-E device/port type */    /* PCI-E设备/端口类型 */
    u8      rom_base_reg;   /* which config register controls the ROM */    /* 使用哪个配置寄存器来控制ROM */
    u8      pin;        /* which interrupt pin this device uses */  /* 设备使用的中断针脚 */

    struct pci_driver *driver;  /* which driver has allocated this device */   /* 分配当前device实例的驱动程序 */
...
    struct  device  dev;        /* Generic device interface */  /* 到通用设备模型的接口 */

    /* device is compatible with these IDs */
    unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];
    unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];

    int     cfg_size;   /* Size of configuration space */   /* 配置空间的长度 */

    /*
     * Instead of touching interrupt line and base address registers
     * directly, use the values stored here. They might be different!
     */
    unsigned int    irq;
    struct resource resource[DEVICE_COUNT_RESOURCE]; /* I/O and memory regions + expansion ROMs */
...
}
驱动程序函数
struct pci_driver {
    struct list_head node;
    char *name;
    const struct pci_device_id *id_table;   /* must be non-NULL for probe to be called */    //(子)设备和(子)厂商ID用于在一个列表中 唯一地标识所支持的设备,内核使用该列表来确定驱动程序所支持的设备
    int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);   /* New device inserted */ //检查该驱动程序是否支持某个PCI设备
    void (*remove) (struct pci_dev *dev);   /* Device removed (NULL if not a hot-plug capable driver) */ //用于移除设备
    int  (*suspend) (struct pci_dev *dev, pm_message_t state);  /* Device suspended */
    int  (*suspend_late) (struct pci_dev *dev, pm_message_t state);
    int  (*resume_early) (struct pci_dev *dev);
    int  (*resume) (struct pci_dev *dev);                   /* Device woken up */
    void (*shutdown) (struct pci_dev *dev);

    struct pci_error_handlers *err_handler;
    struct device_driver    driver; //建立与通用设备模型的关联
    struct pci_dynids dynids;
};
注册驱动程序

PCI驱动程序可以通过pci_register_driver注册。该函数十分简单。其主要任务是,对相关函数已经分配的一个pci_device实例,填充一些剩余的字段。该实例使用driver_register传递到通 用设备层,该函数的运作方式在上文讨论过。

04-21 13:01