Linux的PCI驱动介绍(入门)

1. 关键数据结构

PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。

Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。

在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用: 

• pci_driver 

这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ):

struct pci_driver {

    struct list_head node;

    char *name;

    const struct pci_device_id *id_table;

    int  (*probe)  (struct pci_dev *dev, const struct pci_device_id *id);

    void (*remove) (struct pci_dev *dev);

    int  (*save_state) (struct pci_dev *dev, u32 state);

    int  (*suspend)(struct pci_dev *dev, u32 state);

    int  (*resume) (struct pci_dev *dev);

    int  (*enable_wake) (struct pci_dev *dev, u32 state, int enable);

}; 

• pci_dev 

这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等: 

struct pci_dev {

    struct list_head global_list;

    struct list_head bus_list;

    struct pci_bus  *bus;

    struct pci_bus  *subordinate;

    void        *sysdata;

    struct proc_dir_entry *procent;

    unsigned int    devfn;

    unsigned short  vendor;

    unsigned short  device;

    unsigned short  subsystem_vendor;

    unsigned short  subsystem_device;

    unsigned int    class;

    u8      hdr_type;

    u8      rom_base_reg;

    struct pci_driver *driver;

    void        *driver_data;

    u64     dma_mask;

    u32             current_state;

    unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];

    unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];

    unsigned int    irq;

    struct resource resource[DEVICE_COUNT_RESOURCE];

    struct resource dma_resource[DEVICE_COUNT_DMA];

    struct resource irq_resource[DEVICE_COUNT_IRQ];

    char        name[80];

    char        slot_name[8];

    int     active;

    int     ro;

    unsigned short  regs;

    int (*prepare)(struct pci_dev *dev);

    int (*activate)(struct pci_dev *dev);

    int (*deactivate)(struct pci_dev *dev);

}; 

2. 基本框架

在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。

static struct pci_device_id demo_pci_tbl [] __initdata = {

    {PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,

     PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},

    {0,}

};

struct demo_card {

    unsigned int magic;

   

    struct demo_card *next;    

   

}

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

   

}

static struct file_operations demo_fops = {

    owner:      THIS_MODULE,  

    read:       demo_read,    

    write:      demo_write,    

    ioctl:      demo_ioctl,    

    mmap:       demo_mmap,    

    open:       demo_open,    

    release:    demo_release    

   

};

static struct pci_driver demo_pci_driver = {

    name:       demo_MODULE_NAME,    

    id_table:   demo_pci_tbl,    

    probe:      demo_probe,    

    remove:     demo_remove    

   

};

static int __init demo_init_module (void)

{

   

}

static void __exit demo_cleanup_module (void)

{

    pci_unregister_driver(&demo_pci_driver);

}

module_init(demo_init_module);

module_exit(demo_cleanup_module);

上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。

3. 初始化设备模块

在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:

• 检查PCI总线是否被Linux内核支持; 

• 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。 

• 读出配置头中的信息提供给驱动程序使用。 

当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:

static int __init demo_init_module (void)

{

   

    if (!pci_present())

        return -ENODEV;

   

    if (!pci_register_driver(&demo_pci_driver)) {

        pci_unregister_driver(&demo_pci_driver);

                return -ENODEV;

    }

      

    return 0;

}

驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。 

static int __init demo_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)

{

    struct demo_card *card;

   

    if (pci_enable_device(pci_dev))

        return -EIO;

   

    if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {

        return -ENODEV;

    }

   

    if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) {

        printk(KERN_ERR "pci_demo: out of memory\n");

        return -ENOMEM;

    }

    memset(card, 0, sizeof(*card));

   

    card->iobase = pci_resource_start (pci_dev, 1);

    card->pci_dev = pci_dev;

    card->pci_id = pci_id->device;

    card->irq = pci_dev->irq;

    card->next = devs;

    card->magic = DEMO_CARD_MAGIC;

       

    pci_set_master(pci_dev);

   

    request_region(card->iobase, 64, card_names[pci_id->driver_data]);

    return 0;

}

4. 打开设备模块

在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。

static int demo_open(struct inode *inode, struct file *file)

{

   

    request_irq(card->irq, &demo_interrupt, SA_SHIRQ,

        card_names[pci_id->driver_data], card)) {

   

    if(file->f_mode & FMODE_READ) {

       

    }

    if(file->f_mode & FMODE_WRITE) {

       

    }    

   

    down(&card->open_sem);

    while(card->open_mode & file->f_mode) {

        if (file->f_flags & O_NONBLOCK) {

           

            up(&card->open_sem);

            return -EBUSY;

        } else {

           

            card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);

            up(&card->open_sem);

           

            MOD_INC_USE_COUNT;

           

        }

    }

}

5. 数据读写和控制信息模块

PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:

static int demo_ioctl(struct inode *inode, struct file *file,

      unsigned int cmd, unsigned long arg)

{

   

    

    switch(cmd) {

        case DEMO_RDATA:

           

            val = inl(card->iobae + 0x10);            

            return 0;

    }    

   

}

事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等操作,Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。

6. 中断处理模块

PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    struct demo_card *card = (struct demo_card *)dev_id;

    u32 status;

    spin_lock(&card->lock);

   

    status = inl(card->iobase + GLOB_STA);

    if(!(status & INT_MASK)) 

    {

        spin_unlock(&card->lock);

        return;  

    }

   

    outl(status & INT_MASK, card->iobase + GLOB_STA);

    spin_unlock(&card->lock);    

   

}

7. 释放设备模块

释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备模块相反:

static int demo_release(struct inode *inode, struct file *file)

{

       

   

    card->open_mode &= (FMODE_READ | FMODE_WRITE);    

   

    wake_up(&card->open_wait);

    up(&card->open_sem);    

   

    free_irq(card->irq, card);    

   

    MOD_DEC_USE_COUNT;    

     

}

8. 卸载设备模块

卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序: 

static void __exit demo_cleanup_module (void)

{

    pci_unregister_driver(&demo_pci_driver);

}

三、PCI驱动程序实现

1. 关键数据结构

PCI设备上有三种地址空间:PCI的I/O空间、PCI的存储空间和PCI的配置空间。CPU可以访问PCI设备上的所有地址空间,其中I/O空间和存储空间提供给设备驱动程序使用,而配置空间则由Linux内核中的PCI初始化代码使用。内核在启动时负责对所有PCI设备进行初始化,配置好所有的PCI设备,包括中断号以及I/O基址,并在文件/proc/pci中列出所有找到的PCI设备,以及这些设备的参数和属性。

Linux驱动程序通常使用结构(struct)来表示一种设备,而结构体中的变量则代表某一具体设备,该变量存放了与该设备相关的所有信息。好的驱动程序都应该能驱动多个同种设备,每个设备之间用次设备号进行区分,如果采用结构数据来代表所有能由该驱动程序驱动的设备,那么就可以简单地使用数组下标来表示次设备号。

在PCI驱动程序中,下面几个关键数据结构起着非常核心的作用:

  • pci_driver

这个数据结构在文件include/linux/pci.h里,这是Linux内核版本2.4之后为新型的PCI设备驱动程序所添加的,其中最主要的是用于识别设备的id_table结构,以及用于检测设备的函数probe( )和卸载设备的函数remove( ):

struct pci_driver {

   struct list_head node;

    char *name;

    const struct pci_device_id *id_table;

    int (*probe) (struct pci_dev *dev, const struct pci_device_id *id);

    void (*remove) (struct pci_dev *dev);

    int (*save_state) (struct pci_dev *dev, u32 state);

    int (*suspend)(struct pci_dev *dev, u32 state);

    int (*resume) (struct pci_dev *dev);

    int (*enable_wake) (struct pci_dev *dev, u32 state, int enable);

};

  • pci_dev

这个数据结构也在文件include/linux/pci.h里,它详细描述了一个PCI设备几乎所有的硬件信息,包括厂商ID、设备ID、各种资源等:

struct pci_dev {

    struct list_head global_list;

    struct list_head bus_list;

    struct pci_bus *bus;

    struct pci_bus *subordinate;

    void        *sysdata;

    struct proc_dir_entry *procent;

    unsigned int    devfn;

    unsigned short vendor;

    unsigned short device;

    unsigned short subsystem_vendor;

    unsigned short subsystem_device;

    unsigned int    class;

    u8      hdr_type;

    u8      rom_base_reg;

    struct pci_driver *driver;

    void        *driver_data;

    u64     dma_mask;

    u32             current_state;

    unsigned short vendor_compatible[DEVICE_COUNT_COMPATIBLE];

    unsigned short device_compatible[DEVICE_COUNT_COMPATIBLE];

    unsigned int    irq;

    struct resource resource[DEVICE_COUNT_RESOURCE];

    struct resource dma_resource[DEVICE_COUNT_DMA];

    struct resource irq_resource[DEVICE_COUNT_IRQ];

    char        name[80];

    char        slot_name[8];

    int     active;

    int     ro;

    unsigned short regs;

    int (*prepare)(struct pci_dev *dev);

    int (*activate)(struct pci_dev *dev);

    int (*deactivate)(struct pci_dev *dev);

};


 

2. 基本框架

在用模块方式实现PCI设备驱动程序时,通常至少要实现以下几个部分:初始化设备模块、设备打开模块、数据读写和控制模块、中断处理模块、设备释放模块、设备卸载模块。下面给出一个典型的PCI设备驱动程序的基本框架,从中不难体会到这几个关键模块是如何组织起来的。

/* 指明该驱动程序适用于哪一些PCI设备 */

static struct pci_device_id demo_pci_tbl [] __initdata = {

    {PCI_VENDOR_ID_DEMO, PCI_DEVICE_ID_DEMO,

     PCI_ANY_ID, PCI_ANY_ID, 0, 0, DEMO},

    {0,}

};

/* 对特定PCI设备进行描述的数据结构 */

struct demo_card {

    unsigned int magic;

    /* 使用链表保存所有同类的PCI设备 */

    struct demo_card *next;

   

    /* ... */

}

/* 中断处理模块 */

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    /* ... */

}

/* 设备文件操作接口 */

static struct file_operations demo_fops = {

    owner:      THIS_MODULE,   /* demo_fops所属的设备模块 */

    read:       demo_read,    /* 读设备操作*/

    write:      demo_write,    /* 写设备操作*/

    ioctl:      demo_ioctl,    /* 控制设备操作*/

    mmap:       demo_mmap,    /* 内存重映射操作*/

    open:       demo_open,    /* 打开设备操作*/

    release:    demo_release    /* 释放设备操作*/

    /* ... */

};

/* 设备模块信息 */

static struct pci_driver demo_pci_driver = {

    name:       demo_MODULE_NAME,    /* 设备模块名称 */

    id_table:   demo_pci_tbl,    /* 能够驱动的设备列表 */

    probe:      demo_probe,    /* 查找并初始化设备 */

    remove:     demo_remove    /* 卸载设备模块 */

    /* ... */

};

static int __init demo_init_module (void)

{

    /* ... */

}

static void __exit demo_cleanup_module (void)

{

    pci_unregister_driver(&demo_pci_driver);

}

/* 加载驱动程序模块入口 */

module_init(demo_init_module);

/* 卸载驱动程序模块入口 */

module_exit(demo_cleanup_module);

上面这段代码给出了一个典型的PCI设备驱动程序的框架,是一种相对固定的模式。需要注意的是,同加载和卸载模块相关的函数或数据结构都要在前面加上__init、__exit等标志符,以使同普通函数区分开来。构造出这样一个框架之后,接下去的工作就是如何完成框架内的各个功能模块了。

 

3. 初始化设备模块

在Linux系统下,想要完成对一个PCI设备的初始化,需要完成以下工作:

  • 检查PCI总线是否被Linux内核支持;
  • 检查设备是否插在总线插槽上,如果在的话则保存它所占用的插槽的位置等信息。
  • 读出配置头中的信息提供给驱动程序使用。

当Linux内核启动并完成对所有PCI设备进行扫描、登录和分配资源等初始化操作的同时,会建立起系统中所有PCI设备的拓扑结构,此后当PCI驱动程序需要对设备进行初始化时,一般都会调用如下的代码:

static int __init demo_init_module (void)

{

    /* 检查系统是否支持PCI总线 */

    if (!pci_present())

        return -ENODEV;

    /* 注册硬件驱动程序 */

    if (!pci_register_driver(&demo_pci_driver)) {

        pci_unregister_driver(&demo_pci_driver);

                return -ENODEV;

    }

    /* ... */

  

    return 0;

}

驱动程序首先调用函数pci_present( )检查PCI总线是否已经被Linux内核支持,如果系统支持PCI总线结构,这个函数的返回值为0,如果驱动程序在调用这个函数时得到了一个非0的返回值,那么驱动程序就必须得中止自己的任务了。在2.4以前的内核中,需要手工调用pci_find_device( )函数来查找PCI设备,但在2.4以后更好的办法是调用pci_register_driver( )函数来注册PCI设备的驱动程序,此时需要提供一个pci_driver结构,在该结构中给出的probe探测例程将负责完成对硬件的检测工作。

static int __init demo_probe(struct pci_dev *pci_dev, const struct pci_device_id *pci_id)

{

    struct demo_card *card;

    /* 启动PCI设备 */

    if (pci_enable_device(pci_dev))

        return -EIO;

    /* 设备DMA标识 */

    if (pci_set_dma_mask(pci_dev, DEMO_DMA_MASK)) {

        return -ENODEV;

    }

    /* 在内核空间中动态申请内存 */

    if ((card = kmalloc(sizeof(struct demo_card), GFP_KERNEL)) == NULL) {

        printk(KERN_ERR "pci_demo: out of memory/n");

        return -ENOMEM;

    }

    memset(card, 0, sizeof(*card));

    /* 读取PCI配置信息 */

    card->iobase = pci_resource_start (pci_dev, 1);

    card->pci_dev = pci_dev;

    card->pci_id = pci_id->device;

    card->irq = pci_dev->irq;

    card->next = devs;

    card->magic = DEMO_CARD_MAGIC;

    /* 设置成总线主DMA模式 */   

    pci_set_master(pci_dev);

    /* 申请I/O资源 */

    request_region(card->iobase, 64, card_names[pci_id->driver_data]);

    return 0;

}

4. 打开设备模块

在这个模块里主要实现申请中断、检查读写模式以及申请对设备的控制权等。在申请控制权的时候,非阻塞方式遇忙返回,否则进程主动接受调度,进入睡眠状态,等待其它进程释放对设备的控制权。
<script type="text/javascript"><!-- google_ad_client = "pub-2299987709779770"; google_ad_width = 468; google_ad_height = 60; google_ad_format = "468x60_as"; google_ad_type = "image"; google_ad_channel =""; google_color_border = "FFFFFF"; google_color_bg = "FFFFFF"; google_color_link = "0000FF"; google_color_url = "008000"; google_color_text = "000000"; //--></script><script type="text/javascript" src="http://pagead2.googlesyndication.com/pagead/show_ads.js"> </script>

static int demo_open(struct inode *inode, struct file *file)

{

    /* 申请中断,注册中断处理程序 */

    request_irq(card->irq, &demo_interrupt, SA_SHIRQ,

        card_names[pci_id->driver_data], card)) {

    /* 检查读写模式 */

    if(file->f_mode & FMODE_READ) {

        /* ... */

    }

    if(file->f_mode & FMODE_WRITE) {

       /* ... */

    }

   

    /* 申请对设备的控制权 */

    down(&card->open_sem);

    while(card->open_mode & file->f_mode) {

        if (file->f_flags & O_NONBLOCK) {

            /* NONBLOCK模式,返回-EBUSY */

            up(&card->open_sem);

            return -EBUSY;

        } else {

            /* 等待调度,获得控制权 */

            card->open_mode |= f_mode & (FMODE_READ | FMODE_WRITE);

            up(&card->open_sem);

            /* 设备打开计数增1 */

            MOD_INC_USE_COUNT;

            /* ... */

        }

    }

}

5. 数据读写和控制信息模块

PCI设备驱动程序可以通过demo_fops 结构中的函数demo_ioctl( ),向应用程序提供对硬件进行控制的接口。例如,通过它可以从I/O寄存器里读取一个数据,并传送到用户空间里:

static int demo_ioctl(struct inode *inode, struct file *file, unsigned int cmd, unsigned long arg)

{

    /* ... */

   

    switch(cmd) {

        case DEMO_RDATA:

            /* 从I/O端口读取4字节的数据 */

            val = inl(card->iobae + 0x10);

            

/* 将读取的数据传输到用户空间 */

            return 0;

    }

   

    /* ... */

}

事实上,在demo_fops里还可以实现诸如demo_read( )、demo_mmap( )等操作,Linux内核源码中的driver目录里提供了许多设备驱动程序的源代码,找那里可以找到类似的例子。在对资源的访问方式上,除了有I/O指令以外,还有对外设I/O内存的访问。对这些内存的操作一方面可以通过把I/O内存重新映射后作为普通内存进行操作,另一方面也可以通过总线主DMA(Bus Master DMA)的方式让设备把数据通过DMA传送到系统内存中。

 

6. 中断处理模块

PC的中断资源比较有限,只有0~15的中断号,因此大部分外部设备都是以共享的形式申请中断号的。当中断发生的时候,中断处理程序首先负责对中断进行识别,然后再做进一步的处理。

static void demo_interrupt(int irq, void *dev_id, struct pt_regs *regs)

{

    struct demo_card *card = (struct demo_card *)dev_id;

    u32 status;

    spin_lock(&card->lock);

    /* 识别中断 */

    status = inl(card->iobase + GLOB_STA);

    if(!(status & INT_MASK))

    {

        spin_unlock(&card->lock);

        return; /* not for us */

    }

    /* 告诉设备已经收到中断 */

    outl(status & INT_MASK, card->iobase + GLOB_STA);

    spin_unlock(&card->lock);

   

    /* 其它进一步的处理,如更新DMA缓冲区指针等 */

}


 

7. 释放设备模块

释放设备模块主要负责释放对设备的控制权,释放占用的内存和中断等,所做的事情正好与打开设备模块相反:

static int demo_release(struct inode *inode, struct file *file)

{

    /* ... */

   

    /* 释放对设备的控制权 */

    card->open_mode &= (FMODE_READ | FMODE_WRITE);

   

    /* 唤醒其它等待获取控制权的进程 */

    wake_up(&card->open_wait);

    up(&card->open_sem);

   

    /* 释放中断 */

    free_irq(card->irq, card);

   

    /* 设备打开计数增1 */

    MOD_DEC_USE_COUNT;

   

    /* ... */ 

}


 

8. 卸载设备模块

卸载设备模块与初始化设备模块是相对应的,实现起来相对比较简单,主要是调用函数pci_unregister_driver( )从Linux内核中注销设备驱动程序:

static void __exit demo_cleanup_module (void)

{

    pci_unregister_driver(&demo_pci_driver);

}


四、小结

PCI总线不仅是目前应用广泛的计算机总线标准,而且是一种兼容性最强、功能最全的计算机总线。而Linux作为一种新的操作系统,其发展前景是无法估量的,同时也为PCI总线与各种新型设备互连成为可能。由于Linux源码开放,因此给连接到PCI总线上的任何设备编写驱动程序变得相对容易。本文介绍如何编译Linux下的PCI驱动程序,针对的内核版本是2.4。

在LINUX的时钟中断中涉及至二个全局变量一个是xtime,它是timeval数据结构变量,另一个则是jiffies,首先看timeval结构
struct timeval
{
    time_t tv_sec; /***second***/
    susecond_t tv_usec;/***microsecond***/
}
到底microsecond是毫秒还是微秒??

1秒=1000毫秒(3个零),1秒=1000 000微秒(6个零),1秒=1000 000 000纳秒(9个零),1秒=1000 000 000 000皮秒(12个零)。
秒用s表现,毫秒用ms,微秒用us表示,纳秒用ns表示,皮秒用ps表示,他们的分级单位是千,即每次3个零。
混淆的原因找到了,由于毫秒用ms表示,所以我老是以为microsecond是毫秒,所以就把tv_usec理解错了。
microsecond查词霸也是微秒的意思(microsecond!=ms,microsecond==us),看来单位的表示迷惑了我,也迷惑了大多数人,请朋友们牢记这里,非常重要。

xtime是从cmos电路中取得的时间,一般是从某一历史时刻开始到现在的时间,也就是为了取得我们操作系统上显示的日期。这个就是所谓的“实时时钟”,它的精确度是微秒。


jiffies是记录着从电脑开机到现在总共的时钟中断次数。在linux内核中jiffies远比xtime重要,那么他取决于系统的频率,单位是Hz,这里不得不说一下频率的单位,1MHz=1000,000Hz(6个零),1KHz=1000Hz(3个零).
频率是周期的倒数,一般是一秒钟中断产生的次数,所以,假如我们需要知道系统的精确的时间单位时,需要换算了,假如我们系统的频率是200Mhz,那么一次中断的间隔是1秒/200,000,000Hz=0.000 000 005秒看一下上面我们的时间单位,对照一下小数点后面是9个零,所以理论上我们系统的精确度是5纳秒。LINUX系统时钟频率是一个常数HZ来决定的,通常HZ=100,那么他的精度度就是10ms(毫秒)。也就是说每10ms一次中断。所以一般来说Linux的精确度是10毫秒。

硬件给内核提供一个系统定时器用以计算和管理时间,内核通过编程预设系统定时器的频率,即节拍率(tick rate),每一个周期称作一个tick(节拍)。Linux内核从2.5版内核开始把频率从100调高到1000,时间单位 jiffies 有多长?


"在 Linux 2.6 中,系统时钟每 1 毫秒中断一次(时钟频率,用 HZ 宏表示,定义为 1000,即每秒中断 1000 次,2.4 中定义为 100,很多应用程序也仍然沿用 100 的时钟频率),这个时间单位称为一个 jiffie。"
"jiffies 与绝对时间之间的转换, 用两个宏来完成两种时间单位的互换:JIFFIES_TO_NS()、NS_TO_JIFFIES()"
(当然带来了很多优点,也有一些缺点).

硬件给内核提供一个系统定时器用以计算和管理时间,内核通过编程预设系统定时器的频率,即节拍率(tick rate),每一个周期称作一个tick(节拍)。Linux内核从2.5版内核开始把频率从100调高到1000(当然带来了很多优点,也有一些缺点).
   jiffies是内核中的一个全局变量,用来记录自系统启动一来产生的节拍数。譬如,如果计算系统运行了多长时间,可以用 jiffies/tick rate 来计算。jiffies定义在文件<linux/jiffies.h>中:

extern unsigned long volatile jiffies;

    可以利用jiffies设置超时等,譬如:

unsigned long timeout = jiffies + tick_rate * 2; // 2秒钟后超时

   
    if(time_before(jiffies, timeout){
       // 还没有超时

    }
    else{
       // 已经超时

    }

  
   
内核提供了四个宏来比较节拍计数,这些宏定义在文件<linux/jiffies.h>中:

    time_before(unknown, known)
    time_after(unknown, known)
    time_before_eq(unknown, known)
    time_after_eq(unknown, known)

    比较的时候用这些宏可以避免jiffies由于过大造成的回绕问题。


    除了系统定时器外,还有一个与时间有关的时钟:实时时钟(RTC),这是一个硬件时钟,用来持久存放系统时间,系统关闭后靠主板上的微型电池保持计时。系统启动时,内核通过读取RTC来初始化Wall Time,并存放在xtime变量中,这是RTC最主要的作用。

       ///网络相关函数内容详解//

 
1.linux HZ

Linux核心几个重要跟时间有关的名词或变数,以下将介绍HZ、tick与jiffies。

HZ

Linux核心每隔固定周期会发出timer interrupt (IRQ 0),HZ是用来定义每一秒有几次timer interrupts。举例来说,HZ为1000,代表每秒有1000次timer interrupts。 HZ可在编译核心时设定,如下所示(以核心版本2.6.20-15为例):

desktop:~$ cd /usr/src/linux

desktop:/usr/src/linux$ make menuconfig

Processor type and features    Timer frequency (250 HZ) 

其中HZ可设定100   250  300或1000

小实验

观察/proc/interrupt的timer中断次数,并于一秒后再次观察其值。理论上,两者应该相差250左右。

adrian@adrian-desktop:~$ cat /proc/interrupts | grep timer && sleep 1 && cat /proc/interrupts | grep timer

0: 9309306 IO-APIC-edge timer

0: 9309562 IO-APIC-edge timer

上面四个栏位分别为中断号码、CPU中断次数、PIC与装置名称。

要检查系统上HZ的值是什么,就执行命令

cat kernel/.config | grep '^CONFIG_HZ='


2.Tick

Tick是HZ的倒数,意即timer interrupt每发生一次中断的时间。如HZ为250时,tick为4毫秒(millisecond)。


3.Jiffies

Jiffies为Linux核心变数(unsigned long),它被用来记录系统自开机以来,已经过了多少tick。每发生一次timer interrupt,Jiffies变数会被加一。值得注意的是,Jiffies于系统开机时,并非初始化成零,而是被设为-300*HZ (arch/i386/kernel/time.c),即代表系统于开机五分钟后,jiffies便会溢位。那溢位怎么办?事实上,Linux核心定义几个macro(timer_after、time_after_eq、time_before与time_before_eq),即便是溢位,也能借由这几个macro正确地取得jiffies的内容。

另外,80x86架构定义一个与jiffies相关的变数jiffies_64 ,此变数64位元,要等到此变数溢位可能要好几百万年。因此要等到溢位这刻发生应该很难吧。


3.1 jiffies及其溢出

全局变量jiffies取值为自操作系统启动以来的时钟滴答的数目,在头文件<linux/sched.h>中定义,数据类型为unsigned long volatile (32位无符号长整型)。

jiffies转换为秒可采用公式:(jiffies/HZ)计算,

将秒转换为jiffies可采用公式:(seconds*HZ)计算。

当时钟中断发生时,jiffies 值就加1。因此连续累加一年又四个多月后就会溢出(假定HZ=100,1个jiffies等于1/100秒,jiffies可记录的最大秒数为 (2^32 -1)/100=42949672.95秒,约合497天或1.38年),即当取值到达最大值时继续加1,就变为了0。


3.4  Linux内核如何来防止jiffies溢出

Linux内核中提供了以下四个宏,可有效解决由于jiffies溢出而造成程序逻辑出错的情况。下面是从Linux Kernel 2.6.7版本中摘取出来的代码:

/*

* These inlines deal with timer wrapping correctly. You are

* strongly encouraged to use them

* 1. Because people otherwise forget

* 2. Because if the timer wrap changes in future you won't have to

* alter your driver code.

*

* time_after(a,b) returns true if the time a is after time b.

*

* Do this with "<0" and ">=0" to only test the sign of the result. A

* good compiler would generate better code (and a really good compiler

* wouldn't care). Gcc is currently neither.

*/

#define time_after(a,b) \

(typecheck(unsigned long, a) && \

typecheck(unsigned long, b) && \

((long)(b) - (long)(a) < 0))

#define time_before(a,b) time_after(b,a)

#define time_after_eq(a,b) \

(typecheck(unsigned long, a) && \

typecheck(unsigned long, b) && \

((long)(a) - (long)(b) >= 0))

#define time_before_eq(a,b) time_after_eq(b,a)

在宏time_after中,首先确保两个输入参数a和b的数据类型为unsigned long,然后才执行实际的比较。

8. 结论

系统中采用jiffies来计算时间,但由于jiffies溢出可能造成时间比较的错误,因而强烈建议在编码中使用 time_after等宏来比较时间先后关系,这些宏可以放心使用。

内核时钟:

内核使用硬件提供的不同时钟来提供依赖于时间的服务,如busy-waiting(浪费CPU周期)和sleep-waiting(放弃CPU)


5.HZ and Jiffies

      jiffies记录了系统启动后的滴答数,常用的函数:time_before()、 time_after()、time_after_eq()、time_before_eq()。因为jiffies随时钟滴答变化,不能用编译器优化它,应取volatile值。

      32位jiffies变量会在50天后溢出,太小,因此内核提供变量jiffies_64来hold 64位jiffies。该64位的低32位即为jiffies,在32位机上需要两天指令来赋值64位数据,不是原子的,因此内核提供函数 get_jiffies_64()。


6.Long Delays

    busy-wait:timebefore(),使CPU忙等待;sleep-wait:shedule_timeout(截至时间);无论在内核空间还是用户空间,都没有比HZ更精确的控制了,因为时间片都是根据滴答更新的,而且即使定义了您的进程在超过指定时间后运行,调度器也可能根据优先级选择其他进程执行。

    sleep-wait():wait_event_timeout()用于在满足某个条件或超时后重新执行,msleep()睡眠指定的ms后重新进入就绪队列,这些长延迟仅适用于进程上下文,在中断上下文中不能睡眠也不能长时间busy-waiting。

内核提供了timer API来在一定时间后执行某个函数:

#include <linux/timer.h>

struct timer_list my_timer;

init_timer(&my_timer);            /* Also see setup_timer() */

my_timer.expire = jiffies + n*HZ; /* n is the timeout in number                                    of seconds */

my_timer.function = timer_func;   /* Function to execute

                                     after n seconds */

my_timer.data = func_parameter;   /* Parameter to be passed                                   to timer_func */

add_timer(&my_timer);                /*Start the timer*/

如果您想周期性执行上述代码,那么把它们加入timer_func()函数。您使用mod_timer()来改变my_timer的超时值,del_timer()来删掉my_timer,用timer_pending()查看是否my_timer处于挂起状态。

    用户空间函数clock_settime()和clock_gettime()用于获取内核时钟服务。用户应用程序使用setitimer()和getitimer()来控制alarm信号的传递当指定超时发生后。


8.Real Time Clock

     RTC时钟track绝对时间。RTC电池常超过computer生存期。可以用RTC完成以下功能:(1)读或设置绝对时钟,并在clock updates时产生中断;(2)以2HZ到8192HZ来产生周期性中断;(3)设置alarms。

    jiffies仅是相对于系统启动的相对时间,如果想获取absolute time或wall time,则需要使用RTC,内核用变量xtime来记录,当系统启动时,读取RTC并记录在xtime中,当系统halt时,则将wall time写回RTC,函数do_gettimeofday()来读取wall time。

#include <linux/time.h>

static struct timeval curr_time;

do_gettimeofday(&curr_time);

my_timestamp = cpu_to_le32(curr_time.tv_sec); /* Record timestamp */

    用户空间获取wall time的函数:time()返回calendar time或从00:00:00 on January 1,1970的秒数;(2)localtime():返回calendar time in broken-down format;(3)mktime():与 localtime()相反;(4)gettimeofday()以microsecond 精确度返回calendar时间。

    另外一个获取RTC的方法是通过字符设备/dev/rtc,一个时刻仅允许一个处理器访问它。


9.时钟和定时器

时钟和定时器对Linux内核来说十分重要。首先,内核要管理系统的运行时间(uptime)和当前墙上时间(wall time), 即当前实际时间。其次,内核中大量的活动由时间驱动。


9.1实时时钟

    内核必须借助硬件来实现时间管理。实时时钟是用来持久存放系统时间的设备,它通过主板电池供电,所以即便在关闭计算机系统之后,实时时钟仍然能继续工作。

    系统启动时,内核读取实时时钟,将所读的时间存放在变量xtime中作为墙上时间(wall time),xtime保存着从1970年1月1日0:00到当前时刻所经历的秒数。虽然在Intel x86机器上,内核会周期性地将当前时间存回实时时钟中,但应该明确,实时时钟的主要作用就是在启动时初始化墙上时间xtime。


9.2系统定时器与动态定时器

    周期性发生的事件都是由系统定时器驱动。在X86体系结构上,系统定时器通常是一种可编程硬件芯片,其产生的中断就是时钟中断。时钟中断对应的处理程序负责更新系统时间和执行周期性运行的任务。系统定时器的频率称为节拍率(tick rate),在内核中表示为HZ。

    以X86为例,在2.4之前的内核中其大小为100; 从内核2.6开始,HZ = 1000, 也就是说每秒时钟中断发生1000次。这一变化使得系统定时器的精度(resolution)由10ms提高到1ms,这大大提高了系统对于时间驱动事件调度的精确性。过于频繁的时钟中断不可避免地增加了系统开销。

    与系统定时器相对的是动态定时器,它是调度事件(执行调度程序)在未来某个时刻发生的时机。内核可以动态地创建或销毁动态定时器。

    系统定时器及其中断处理程序是内核管理机制的中枢,下面是一些利用系统定时器周期执行的工作(中断处理程序所做的工作):

    (1) 更新系统运行时间(uptime)

    (2) 更新当前墙上时间(wall time)

    (3) 在对称多处理器系统(SMP)上,均衡调度各处理器上的运行队列

    (4) 检查当前进程是否用完了时间片(time slice),如果用尽,则进行重新调度

    (5) 运行超时的动态定时器

    (6) 更新资源耗尽和处理器时间的统计值

    内核动态定时器依赖于系统时钟中断,因为只有在系统时钟中断发生后内核才会去检查当前是否有超时的动态定时器。

---------------------------------------------------------

    X86体系结构中,内核2.6.X的HZ = 1000, 即系统时钟中断执行粒度为1ms,这意味着系统中周期事情最快为1ms执行一次,而不可能有更高的精度。动态定时器随时都可能超时,但由于只有在系统时钟中断到来时内核才会检查执行超时的动态定时器,所以动态定时器的平均误差大约为半个系统时钟周期(即0.5ms).


版权声明:本文为flyingleo1981原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。