• 前面我们已经学习了platform设备的理论知识Linux 设备驱动开发 —— platform 设备驱动 ,下面将通过一个实例来深入我们的学习。   一、platform 驱动的工作过程  platform模型驱动编程,需要实现platform_device...

           前面我们已经学习了platform设备的理论知识Linux 设备驱动开发 —— platform 设备驱动 ,下面将通过一个实例来深入我们的学习。

           

    一、platform 驱动的工作过程

            platform模型驱动编程,需要实现platform_device(设备)platform_driver(驱动)platform(虚拟总线)上的注册、匹配,相互绑定,然后再做为一个普通的字符设备进行相应的应用,总之如果编写的是基于字符设备的platform驱动,在遵循并实现platform总线上驱动与设备的特定接口的情况下,最核心的还是字符设备的核心结构:cdev、 file_operations(他包含的操作函数接口)、dev_t(设备号)、设备文件(/dev)等,因为用platform机制编写的字符驱动,它的本质是字符驱动。

          我们要记住,platform 驱动只是在字符设备驱动外套一层platform_driver 的外壳

         在一般情况下,2.6内核中已经初始化并挂载了一条platform总线在sysfs文件系统中。那么我们编写platform模型驱动时,需要完成两个工作:

    a -- 实现platform驱动 

    b -- 实现platform设备

         然而在实现这两个工作的过程中还需要实现其他的很多小工作,在后面介绍。platform模型驱动的实现过程核心架构就很简单,如下所示:


    platform驱动模型三个对象:platform总线platform设备platform驱动

    platform总线对应的内核结构:struct bus_type-->它包含的最关键的函数:match() (要注意的是,这块由内核完成,我们不参与)

    platform设备对应的内核结构:struct platform_device-->注册:platform_device_register(unregister)

    platform驱动对应的内核结构:struct platform_driver-->注册:platform_driver_register(unregister)

           

    那具体platform驱动的工作过程是什么呢:

         设备(或驱动)注册的时候,都会引发总线调用自己的match函数来寻找目前platform总线是否挂载有与该设备(或驱动)名字匹配的驱动(或设备),如果存在则将双方绑定;

         如果先注册设备,驱动还没有注册,那么设备在被注册到总线上时,将不会匹配到与自己同名的驱动,然后在驱动注册到总线上时,因为设备已注册,那么总线会立即匹配与绑定这时的同名的设备与驱动,再调用驱动中的probe函数等;

        如果是驱动先注册,同设备驱动一样先会匹配失败,匹配失败将导致它的probe函数暂不调用,而是要等到设备注册成功并与自己匹配绑定后才会调用



    二、实现platform 驱动与设备的详细过程

    1、思考问题?

          在分析platform 之前,可以先思考一下下面的问题:

    a -- 为什么要用 platform 驱动?不用platform驱动可以吗?

    b -- 设备驱动中引入platform 概念有什么好处?

            现在先不回答,看完下面的分析就明白了,后面会附上总结。


    2、platform_device 结构体 VS platform_driver 结构体

          这两个结构体分别描述了设备和驱动,二者有什么关系呢?先看一下具体结构体对比

    设备(硬件部分):中断号,寄存器,DMA等
                       platform_device 结构体
     驱动(软件部分)
                             platform_driver 结构体       
    struct platform_device {
        const char    *name;       名字
        int        id;
        bool        id_auto;
        struct device    dev;   硬件模块必须包含该结构体
        u32        num_resources;        资源个数
        struct resource    *resource;         资源  人脉
        const struct platform_device_id    *id_entry;
        /* arch specific additions */
        struct pdev_archdata    archdata;
    };
    struct platform_driver {
        int (*probe)(struct platform_device *);
        硬件和软件匹配成功之后调用该函数
        int (*remove)(struct platform_device *);
        硬件卸载了调用该函数
        void (*shutdown)(struct platform_device *);
        int (*suspend)(struct platform_device *, pm_message_t state);
        int (*resume)(struct platform_device *);
        struct device_driver driver;内核里所有的驱动程序必须包含该结构体
        const struct platform_device_id *id_table;  八字
    };
    设备实例:
    static struct platform_device hello_device=
    {
        .name = "bigbang",
        .id = -1,
        .dev.release = hello_release,
    };
    驱动实例:
    static struct platform_driver hello_driver=
    {
        .driver.name = "bigbang",
        .probe = hello_probe,
        .remove = hello_remove,
    };

           前面提到,实现platform模型的过程就是总线对设备和驱动的匹配过程 。打个比方,就好比相亲,总线是红娘,设备是男方,驱动是女方

    a -- 红娘(总线)负责男方(设备)和女方(驱动)的撮合;     

    b -- 男方(女方)找到红娘,说我来登记一下,看有没有合适的姑娘(汉子)—— 设备或驱动的注册

    c -- 红娘这时候就需要看看有没有八字(二者的name 字段)匹配的姑娘(汉子)——match 函数进行匹配,看name是否相同;

    d -- 如果八字不合,就告诉男方(女方)没有合适的对象,先等着,别急着乱做事 —— 设备和驱动会等待,直到匹配成功;

    e -- 终于遇到八字匹配的了,那就结婚呗!接完婚,男方就向女方交代,我有多少存款,我的房子在哪,钱放在哪等等( struct resource    *resource),女方说好啊,于是去房子里拿钱,去给男方买菜啦,给自己买衣服、化妆品、首饰啊等等(int (*probe)(struct platform_device *) 匹配成功后驱动执行的第一个函数),当然如果男的跟小三跑了(设备卸载),女方也不会继续待下去的(  int (*remove)(struct platform_device *))。


    3、设备资源结构体

          在struct platform_device 结构体中有一重要成员 struct resource *resource

    struct resource {
    	resource_size_t start;  资源起始地址   
    	resource_size_t end;   资源结束地址
    	const char *name;      
    	unsigned long flags;   区分是资源什么类型的
    	struct resource *parent, *sibling, *child;
    };
    
    #define IORESOURCE_MEM        0x00000200
    #define IORESOURCE_IRQ        0x00000400   

           flags 指资源类型,我们常用的是 IORESOURCE_MEM、IORESOURCE_IRQ  这两种。start 和 end 的含义会随着 flags而变更,如

    a -- flags为IORESOURCE_MEM 时,start 、end 分别表示该platform_device占据的内存的开始地址和结束值;  

    b -- flags为 IORESOURCE_IRQ   时,start 、end 分别表示该platform_device使用的中断号的开始地址和结束值; 

    下面看一个实例:

    static struct  resource beep_resource[] =
    {
    	[0] = {
            	.start = 0x114000a0,
    		.end = 0x114000a0+0x4,
            	.flags = IORESOURCE_MEM,
    	},
    
    	[1] = {
            	.start = 0x139D0000,
            	.end = 0x139D0000+0x14,
            	.flags = IORESOURCE_MEM,
    	},
    };

    4、将字符设备添加到 platform的driver中
          前面我们提到platform 驱动只是在字符设备驱动外套一层platform_driver 的外壳,下面我们看一下添加的过程:
    static struct file_operations hello_ops=
    {
    	.open = hello_open,
    	.release = hello_release,
    	.unlocked_ioctl = hello_ioctl,
    };
    
    static int hello_remove(struct platform_device *pdev)
    {
    	注销分配的各种资源
    }
    
    static int hello_probe(struct platform_device *pdev)
    {
    	1.申请设备号
    	2.cdev初始化注册,&hello_ops
    	3.从pdev读出硬件资源
    	4.对硬件资源初始化,ioremap,request_irq( )
    }
    
    static int hello_init(void)
    {
    	只注册 platform_driver
    }
    
    static void hello_exit(void)
    {
    	只注销 platform_driver
    }
          可以看到,模块加载和卸载函数仅仅通过paltform_driver_register()、paltform_driver_unregister() 函数进行 platform_driver 的注册和注销,而原先注册和注销字符设备的工作已经被移交到 platform_driver 的 probe() 和 remove() 成员函数中

    5、platform是如何匹配device和driver
          这时就该总线出场了,系统为platform总线定义了一个bus_type 的实例platform_bus_type,其定义如下:
    struct bus_type platform_bus_type = {
    	.name        = "platform",
    	.dev_groups    = platform_dev_groups,
    	.match        = platform_match,
    	.uevent        = platform_uevent,
    	.pm        = &platform_dev_pm_ops,
    };
          其又是怎样工作的呢?在platform.c (e:\linux-3.14-fs4412\drivers\base)    31577    2014/3/31 中可以看到
    __platform_driver_register()
    {
    	drv->driver.bus = &platform_bus_type;     536行
    }
    
        在 platform_bus_type 中调用 了platform_match:
    static int platform_match(struct device *dev, struct device_driver *drv)
    {
    	struct platform_device *pdev = to_platform_device(dev);
    	struct platform_driver *pdrv = to_platform_driver(drv);
    
    	匹配设备树信息,如果有设备树,就调用 of_driver_match_device() 函数进行匹配
    	if (of_driver_match_device(dev, drv))
    		return 1;
    
    
    	匹配id_table
    	if (pdrv->id_table)
    		return platform_match_id(pdrv->id_table, pdev) != NULL;
    
    	最基本匹配规则
    	return (strcmp(pdev->name, drv->name) == 0);
    }
    


    6、解决问题

          现在可以回答这两个问题了

    a -- 为什么要用 platform 驱动?不用platform驱动可以吗?

    b -- 设备驱动中引入platform 概念有什么好处?

          引入platform模型符合Linux 设备模型 —— 总线、设备、驱动,设备模型中配套的sysfs节点都可以用,方便我们的开发;当然你也可以选择不用,不过就失去了一些platform带来的便利;

          设备驱动中引入platform 概念,隔离BSP和驱动。在BSP中定义platform设备和设备使用的资源、设备的具体匹配信息,而在驱动中,只需要通过API去获取资源和数据,做到了板相关代码和驱动代码的分离,使得驱动具有更好的可扩展性和跨平台性。


    三、实例

            这是一个蜂鸣器的驱动,其实前面已经有解析 Linux 字符设备驱动开发基础(二)—— 编写简单 PWM 设备驱动, 下面来看一下,套上platform 外壳后的程序:

    1、device.c

    #include <linux/module.h>
    #include <linux/device.h>
    #include <linux/platform_device.h>
    #include <linux/ioport.h>
    
    static struct resource beep_resource[] =
    {
    	[0] ={
    		.start = 0x114000a0,
    		.end =  0x114000a0 + 0x4,
    		.flags = IORESOURCE_MEM,
    	},
    
    	[1] ={
    		.start = 0x139D0000,
    		.end =  0x139D0000 + 0x14,
    		.flags = IORESOURCE_MEM,
    	}
    };
    
    static void hello_release(struct device *dev)
    {
    	printk("hello_release\n");
    	return ;
    }
    
    
    
    static struct platform_device hello_device=
    {
        .name = "bigbang",
        .id = -1,
        .dev.release = hello_release,
        .num_resources = ARRAY_SIZE(beep_resource),
        .resource = beep_resource,
    };
    
    static int hello_init(void)
    {
    	printk("hello_init");
    	return platform_device_register(&hello_device);
    }
    
    static void hello_exit(void)
    {
    	printk("hello_exit");
    	platform_device_unregister(&hello_device);
    	return;
    }
    
    MODULE_LICENSE("GPL");
    module_init(hello_init);
    module_exit(hello_exit);
    

    2、driver.c

    #include <linux/module.h>
    #include <linux/fs.h>
    #include <linux/cdev.h>
    #include <linux/device.h>
    #include <linux/platform_device.h>
    #include <asm/io.h>
    
    static int major = 250;
    static int minor=0;
    static dev_t devno;
    static struct class *cls;
    static struct device *test_device;
             
    #define TCFG0         0x0000               
    #define TCFG1         0x0004                            
    #define TCON          0x0008             
    #define TCNTB0        0x000C          
    #define TCMPB0        0x0010           
    
    static unsigned int *gpd0con;
    static void *timer_base;
    
    #define  MAGIC_NUMBER    'k'
    #define  BEEP_ON    _IO(MAGIC_NUMBER    ,0)
    #define  BEEP_OFF   _IO(MAGIC_NUMBER    ,1)
    #define  BEEP_FREQ   _IO(MAGIC_NUMBER   ,2)
    
    static void fs4412_beep_init(void)
    {	
    	writel ((readl(gpd0con)&~(0xf<<0)) | (0x2<<0),gpd0con);
    	writel ((readl(timer_base +TCFG0  )&~(0xff<<0)) | (0xff <<0),timer_base +TCFG0); 
    	writel ((readl(timer_base +TCFG1 )&~(0xf<<0)) | (0x2 <<0),timer_base +TCFG1 ); 
    
    	writel (500, timer_base +TCNTB0  );
    	writel (250, timer_base +TCMPB0 );
    	writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x2 <<0),timer_base +TCON ); 
    }
    
    void fs4412_beep_on(void)
    {
    	writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x9 <<0),timer_base +TCON );
    }
    
    void fs4412_beep_off(void)
    {
    	writel ((readl(timer_base +TCON )&~(0xf<<0)) | (0x0 <<0),timer_base +TCON );
    }
    
    static void beep_unmap(void)
    {
    		iounmap(gpd0con);
    		iounmap(timer_base);
    }
    
    static int beep_open (struct inode *inode, struct file *filep)
    {
    	fs4412_beep_on();
    	return 0;
    }
    
    static int beep_release(struct inode *inode, struct file *filep)
    {
    	 fs4412_beep_off();
    	 return 0;
    }
    
    #define BEPP_IN_FREQ 100000
    static void beep_freq(unsigned long arg)
    {
    	writel(BEPP_IN_FREQ/arg, timer_base +TCNTB0  );
    	writel(BEPP_IN_FREQ/(2*arg), timer_base +TCMPB0 );
    
    }
    
    static long beep_ioctl(struct file *filep, unsigned int cmd, unsigned long arg)
    {
    	switch(cmd)
    	{
    		case BEEP_ON:
    			fs4412_beep_on();
    			break;
    		case BEEP_OFF:
    			fs4412_beep_off();
    			break;
    		case BEEP_FREQ:
    			beep_freq( arg );
    			break;
    		default :
    			return -EINVAL;
    	}
    	return 0;
    }
    
    static struct file_operations beep_ops=
    {
    	.open     = beep_open,
    	.release = beep_release,
    	.unlocked_ioctl      = beep_ioctl,
    };
    
    static int beep_probe(struct platform_device *pdev)
    {
    	int ret;	
    	printk("match ok!");
    	
    	gpd0con = ioremap(pdev->resource[0].start,pdev->resource[0].end - pdev->resource[0].start);
    	timer_base = ioremap(pdev->resource[1].start, pdev->resource[1].end - pdev->resource[1].start);
    
    	devno = MKDEV(major,minor);
    	ret = register_chrdev(major,"beep",&beep_ops);
    
    	cls = class_create(THIS_MODULE, "myclass");
    	if(IS_ERR(cls))
    	{
    		unregister_chrdev(major,"beep");
    		return -EBUSY;
    	}
    
    	test_device = device_create(cls,NULL,devno,NULL,"beep");//mknod /dev/hello
    	if(IS_ERR(test_device))
    	{
    		class_destroy(cls);
    		unregister_chrdev(major,"beep");
    		return -EBUSY;
    	}
    	
    	fs4412_beep_init();
    	
    	return 0;
    }
    
    static int beep_remove(struct platform_device *pdev)
    {
    	beep_unmap();
    	device_destroy(cls,devno);
    	class_destroy(cls);	
    	unregister_chrdev(major,"beep");
    
    	return 0;
    }
    
    
    static struct platform_driver beep_driver=
    {
        .driver.name = "bigbang",
        .probe = beep_probe,
        .remove = beep_remove,
    };
     
    
    static int beep_init(void)
    {
    	printk("beep_init");
    	
    	return platform_driver_register(&beep_driver);
    }
    
    static void beep_exit(void)
    {
    	printk("beep_exit");
    	platform_driver_unregister(&beep_driver);
    	
    	return;
    }
    
    
    MODULE_LICENSE("GPL");
    module_init(beep_init);
    module_exit(beep_exit);
    

    3、makefile   

    ifneq  ($(KERNELRELEASE),)
    obj-m:=device.o driver.o
    $(info "2nd")
    else
    #KDIR := /lib/modules/$(shell uname -r)/build
    KDIR := /home/fs/linux/linux-3.14-fs4412
    PWD:=$(shell pwd)
    all:
    	$(info "1st")
    	make -C $(KDIR) M=$(PWD) modules
    clean:
    	rm -f *.ko *.o *.symvers *.mod.c *.mod.o *.order
    endif
    

    4、test.c
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <fcntl.h>
    #include <stdio.h>
    
    main()
    {
    	int fd,i,lednum;
    
    	fd = open("/dev/beep",O_RDWR);
    	if(fd<0)
    	{
    		perror("open fail \n");
    		return ;
    	}
    	
    	sleep(10);
    	close(fd);
    }
    



    展开全文
  • linux设备驱动开发,看起来是一份很高大上的职业,毕竟从事上层应用开发人员太多,而且门槛又不是特别高,而内核级开发从业人员要少得多,而且资料又较少。有许多刚刚接触到linux设备驱动开发的同仁会感觉非常困惑,...



    IMX6技术交流群:195829497

    物联网实验室:345957209

    Python编程俱乐部:516307649





    linux设备驱动开发,看起来是一份很高大上的职业,毕竟从事上层应用开发人员太多,而且门槛又不是特别高,而内核级开发从业人员要少得多,而且资料又较少。有许多刚刚接触到linux设备驱动开发的同仁会感觉非常困惑,面对复杂的linux内核有一种无从下手的感觉。根据自己之前积累的一些经验,今天就和大家分享一下,让刚刚步入驱动开发的同仁少走一些弯路。

    1.要知道将来要做什么

    学习,都是有目的性的,要么是兴趣使然,要么就是刚性需求,为了找一份好的工作。在这里先和大家聊聊做设备驱动将来可以做哪些方面。我把linux设备驱动开发工作分为两大类,一类是做BSP级的开发,另外一类是做外设驱动的开发。

    BSP的开发指的是板级代码的开发,和CPU是密切相关的,例如I2C/SPI Adapter的驱动.如果使用通用的芯片,比如三星的Exynos,飞思卡尔的I.MX系列,TI的OMAP或者DaVinci系列,基本都会有现成的BSP包,这部分代码通常是芯片厂商提供和大型公司贡献。大家可以看看linux内核源码中/arch/arm/mach-omap,内部很多代码都是诺基亚贡献。做BSP级的开发需要有较深的功底,首先要十分了解CPU特性,另外要使代码有良好的扩展性和复用性,方便后续移植。有这样需求的往往是芯片商或使用专用芯片的设备商。

    外设驱动开发就相对简单一些,都是和特定的外设硬件打交到。通过利用BSP级代码提供的API或者linux提供的更高层的抽象接口来操作硬件。实际上和应用层的开发大同小异。例如操作I2C总线上的EEPROM,实际上的读和写操作都有已经封装好的API来完成.而开发者需要做的是了解外设的特性,通过封装好的API对外设进行操作。新入门的开发者建议从外设驱动开发入手,循序渐进。当然,一个优秀的开发者是即可以做BSP级代码的开发,也能做外设驱动的开发的。

    2.用面向对象的思想去思考

    面向对象,即OO思想,大家应该非常熟悉。linux的内核虽然用面向过程的C语言实现,但是仍然是通过面向对象的思想去设计的。如果从单片机转行做linux设备驱动,会发现和单片机的或者裸机的驱动设计有很大区别。设计linux设备驱动不单单是对硬件设备的操作,更多需要考虑的是扩展性和代码的复用。所以就出现了platform device/driver,i2c device/driver,spi device/driver,抽象出了设备和驱动两部分,使设备细节和驱动分离。另外还出现了一些框架,提供了底层接口的封装,做开发时要习惯用OO思想去设计。当然要记住条条大路通罗马,不使用这些device/driver也可以实现设备驱动,只是不太推荐这样做。

    3.从各驱动框架入手

    linux提供了各种框架(子系统),对底层进行封装,抽象出相同操作的接口,可样可以更好的实现复用。想入门linux驱动开发,可以先从框架入手,掌握API的使用,再逐渐深入研究,从上到下去学习。不要把驱动开发想象的太复杂,实际和英语的完型填空差不多,框架有了,只需要自己去填写操作具体硬件的细节代码而已。

    几个比较重要和常用的框架有:

    GPIO:这个就不用多说了,刚开始接触驱动的基本会练习通过GPIO点亮LED的操作,linux封装了相关的gpio操作接口。

    SPI:学会spi device/driver的用法,以及收发消息API,可以参考一些代码,基本都是相同的套路。

    I2C:学会i2c device/driver的用法,和学习SPI的套路一样。

    PINCTRL:非常重要的一个框架,负责CPU引脚复用,由于现在的CPU都很复杂,一个引脚支持多种复用。

    V4L2:一个非常复杂的视频采集框架,具体可以参考相关的文档。驱动里面有很多例子可供参考,同时提供了模板vivi.c

    Framebuffer:显示相关的框架,熟悉其中API,而且有模板skeletonfb.c。

    DMA Engine: 把DMA操作进行封装,目前驱动代码中关于DMA的操作很多是使用私有的BSP包中的DMA接口,如果支持DMA Engine的话,建议使用DMA Engine。

    中断:比较常用的了,接口不多,很少掌握。

    USB框架:USB框架比较复杂,API较多,可以通过读已有的代码进行学习。

    MTD框架:存储相关比较重要的框架,网上相关的文档很多。

    设备树:设备树是在新的内核里面引进来的,可以把板级代码中的各种device通过设备树文件去描述,动态创建,这样更灵活。其实不要把设备树想象的太复杂,实际和解析JSON,XML一样,各个节点中记录设备相关的信息,提供给驱动使用。

    4.选好参考资料

    推荐大家参考《Essential_Linux_Device_Drivers>>,《linux device driver》,还有linux代码目录中的Documentation也非常重要,另外要学会参考现有的驱动代码。既然是参考资料,只是在用的时候去读,这样效果会更好。

    展开全文
  • * NFS和tftp服务器安装3, 驱动开发中设计到的硬件: * 数字电路知识 * ARM硬件知识 * 熟练使用万用表和示波器 * 看懂芯片手册和原理图 4, linux内核源代码目录结构: * arch/: arch子目录包括了所有和...

    基础性总结

    1, linux驱动一般分为3大类:

    * 字符设备
    * 块设备
    * 网络设备

    2, 开发环境构建:

    * 交叉工具链构建
    * NFS和tftp服务器安装

    3, 驱动开发中设计到的硬件:

    * 数字电路知识
    * ARM硬件知识
    * 熟练使用万用表和示波器
    * 看懂芯片手册和原理图

    4, linux内核源代码目录结构:

    这里写图片描述
    * arch/: arch子目录包括了所有和体系结构相关的核心代码。它的每一个子目录都代表一种支持的体系结构,例如i386就是关于intel cpu及与之相兼容体系结构的子目录。
    * block/: 部分块设备驱动程序;
    * crypto: 常用加密和散列算法(如AES、SHA等),还有一些压缩和CRC校验算法;
    * documentation/: 文档目录,没有内核代码,只是一套有用的文档;
    * drivers/: 放置系统所有的设备驱动程序;每种驱动程序又各占用一个子目录:如,/block 下为块设备驱动程序,比如ide(ide.c)。如果你希望查看所有可能包含文件系统的设备是如何初始化的,你可以看 drivers/block/genhd.c中的device_setup()。
    * fs/: 所有的文件系统代码和各种类型的文件操作代码,它的每一个子目录支持一个文件系统, 例如fat和ext2;
    * include/: include子目录包括编译核心所需要的大部分头文件。与平台无关的头文件在 include/linux子目录下,与 intel cpu相关的头文件在include/asm-i386子目录下,而include/scsi目录则是有关scsi设备的头文件目录;
    * init/: 这个目录包含核心的初始化代码(注:不是系统的引导代码),包含两个文件main.c和Version.c,这是研究核心如何工作的好的起点之一;
    * ipc/: 这个目录包含核心的进程间通讯的代码;
    * kernel/: 主要的核心代码,此目录下的文件实现了大多数linux系统的内核函数,其中最重要的文件当属sched.c;同样,和体系结构相关的代码在arch/i386/kernel下;
    * lib/: 放置核心的库代码;
    * mm/:这个目录包括所有独立于 cpu 体系结构的内存管理代码,如页式存储管理内存的分配和释放等;而和体系结构相关的内存管理代码则位于arch/i386/mm/下;
    * net/: 核心与网络相关的代码;
    * scripts/: 描述文件,脚本,用于对核心的配置;
    * security: 主要是一个SELinux的模块;
    * sound: 常用音频设备的驱动程序等;
    * usr: 实现了用于打包和压缩的cpio;

    5, 内核的五个子系统:

    * 进程调试(SCHED)
    * 内存管理(MM)
    * 虚拟文件系统(VFS)
    * 网络接口(NET)
    * 进程间通信(IPC)

    6, linux内核的编译:

    * 配置内核:make menuconfig,使用后会生成一个.confiig配置文件,记录哪些部分被编译入内核,哪些部分被编译成内核模块。
    * 编译内核和模块的方法:make zImage
    Make modules
    * 执行完上述命令后,在arch/arm/boot/目录下得到压缩的内核映像zImage,在内核各对应目录得到选中的内核模块。

    7, 在linux内核中增加程序

    (直接编译进内核)要完成以下3项工作:
    * 将编写的源代码拷入linux内核源代码相应目录
    * 在目录的Kconifg文件中增加关于新源代码对应项目的编译配置选项
    * 在目录的Makefile文件中增加对新源代码的编译条目

    8, linux下C编程的特点:

    内核下的Documentation/CodingStyle描述了linux内核对编码风格的要求。具体要求不一一列举,以下是要注意的:
    * 代码中空格的应用
    * 当前函数名:
    GNU C预定义了两个标志符保存当前函数的名字,__FUNCTION__保存函数在源码中的名字,__PRETTY_FUNCTION__保存带语言特色的名字。
    由于C99已经支持__func__宏,在linux编程中应该不要使用__FUNCTION__,应该使用__func__
    *内建函数:不属于库函数的其他内建函数的命名通常以__builtin开始。

    9,内核模块

    内核模块主要由如下几部分组成:
    (1) 模块加载函数
    (2) 模块卸载函数
    (3) 模块许可证声明(常用的有Dual BSD/GPL,GPL,等)
    (4) 模块参数(可选)它指的是模块被加载的时候可以传递给它的值,它本身对应模块内部的全局变量。例如P88页中讲到的一个带模块参数的例子:
    insmod book.ko book_name=”GOOD BOOK” num=5000
    (5) 模块导出符号(可选)导出的符号可以被其他模块使用,在使用之前只需声明一下。
    (6) 模块作者等声明信息(可选)
    以下是一个典型的内核模块:

    /*
     * A kernel module: book
     * This example is to introduce module params
     *
     * The initial developer of the original code is Baohua Song
     * <author@linuxdriver.cn>. All Rights Reserved.
     */
    
    #include <linux/init.h>
    #include <linux/module.h>
    
    static char *book_name = “dissecting Linux Device Driver”;
    static int num = 4000;
    
    static int book_init(void)
    {
            printk(KERN_INFO “ book name:%s\n”,book_name);
            printk(KERN_INFO “ book num:%d\n”,num);
            return 0;
    }
    
    static void book_exit(void)
    {
            printk(KERN_INFO “ Book module exit\n “);
    }
    
    module_init(book_init);
    module_exit(book_exit);
    module_param(num, int, S_IRUGO);
    module_param(book_name, charp, S_IRUGO);
    MODULE_AUTHOR(“Song Baohua, author@linuxdriver.cn”);
    MODULE_LICENSE(“Dual BSD/GPL”);
    MODULE_DESCRIPTION(“A simple Module for testing module params”);
    MODULE_VERSION(“V1.0”);

    注意:标有__init的函数在链接的时候都放在.init.text段,在.initcall.init中还保存了一份函数指针,初始化的时候内核会通过这些函数指针调用__init函数,在初始化完成后释放init区段。
    模块编译常用模版:

    KVERS = $(shell uname -r)
    # Kernel modules
    obj-m += book.o
    # Specify flags for the module compilation.
    #EXTRA_CFLAGS=-g -O0
    build: kernel_modules
    kernel_modules:
            make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
    
    clean:
            make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean

    注意要指明内核版本,并且内核版本要匹配——编译模块使用的内核版本要和模块欲加载到的那个内核版本要一致。
    模块中经常使用的命令:

    insmod,lsmod,rmmod
    

    系统调用:

    int open(const char *pathname,int flags,mode_t mode);
    

    flag表示文件打开标志,如:O_RDONLY
    mode表示文件访问权限,如:S_IRUSR(用户可读),S_IRWXG(组可以读、写、执行)

    10,linux文件系统与设备驱动的关系

    这里写图片描述
    应用程序和VFS之间的接口是系统调用,而VFS与磁盘文件系统以及普通设备之间的接口是file_operation结构体成员函数。
    两个重要的函数:
    (1)struct file结构体定义在/linux/include/linux/fs.h(Linux 2.6.11内核)中定义。文件结构体代表一个打开的文件,系统中每个打开的文件在内核空间都有一个关联的struct file。它由内核在打开文件时创建,并传递给在文件上进行操作的任何函数。在文件的所有实例都关闭后,内核释放这个数据结构。在内核创建和驱动源码中,struct file的指针通常被命名为file或filp。
    在驱动开发中,文件读/写模式mode、标志f_flags都是设备驱动关心的内容,而私有数据指针private_data在驱动中被广泛使用,大多被指向设备驱动自定义的用于描述设备的结构体。驱动程序中常用如下类似的代码来检测用户打开文件的读写方式:

    if (file->f_mode & FMODE_WRITE) //用户要求可写
    {
    }
    if (file->f_mode & FMODE_READ) //用户要求可读
    {
    }

    下面的代码可用于判断以阻塞还是非阻塞方式打开设备文件:

    if (file->f_flags & O_NONBLOCK) //非阻塞
    pr_debug("open:non-blocking\n");
    else //阻塞
    pr_debug("open:blocking\n");

    (2)struct inode结构体定义在linux/fs.h中

    11,devfs、sysfs、udev三者的关系:

    (1)devfs
    linux下有专门的文件系统用来对设备进行管理,devfs和sysfs就是其中两种。在2.4内核4一直使用的是devfs,devfs挂载于/dev目录下,提供了一种类似于文件的方法来管理位于/dev目录下的所有设备,我们知道/dev目录下的每一个文件都对应的是一个设备,至于当前该设备存在与否先且不论,而且这些特殊文件是位于根文件系统上的,在制作文件系统的时候我们就已经建立了这些设备文件,因此通过操作这些特殊文件,可以实现与内核进行交互。但是devfs文件系统有一些缺点,例如:不确定的设备映射,有时一个设备映射的设备文件可能不同,例如我的U盘可能对应sda有可能对应sdb;没有足够的主/次设备号,当设备过多的时候,显然这会成为一个问题;/dev目录下文件太多而且不能表示当前系统上的实际设备;命名不够灵活,不能任意指定等等。
    (2)sysfs
    正因为上述这些问题的存在,在linux2.6内核以后,引入了一个新的文件系统sysfs,它挂载于/sys目录下,跟devfs一样它也是一个虚拟文件系统,也是用来对系统的设备进行管理的,它把实际连接到系统上的设备和总线组织成一个分级的文件,用户空间的程序同样可以利用这些信息以实现和内核的交互,该文件系统是当前系统上实际设备树的一个直观反应,它是通过kobject子系统来建立这个信息的,当一个kobject被创建的时候,对应的文件和目录也就被创建了,位于/sys下的相关目录下,既然每个设备在sysfs中都有唯一对应的目录,那么也就可以被用户空间读写了。用户空间的工具udev就是利用了sysfs提供的信息来实现所有devfs的功能的,但不同的是udev运行在用户空间中,而devfs却运行在内核空间,而且udev不存在devfs那些先天的缺陷。
    (3)udev
    udev是一种工具,它能够根据系统中的硬件设备的状况动态更新设备文件,包括设备文件的创建,删除等。设备文件通常放在/dev目录下,使用udev后,在/dev下面只包含系统中真实存在的设备。它于硬件平台无关的,位于用户空间,需要内核sysfs和tmpfs的支持,sysfs为udev提供设备入口和uevent通道,tmpfs为udev设备文件提供存放空间。

    12,linux设备模型:

    这里写图片描述

    在linux内核中,分别使用bus_type,device_driver,device来描述总线、驱动和设备,这3个结构体定义于include/linux/device.h头文件中。驱动和设备正是通过bus_type中的match()函数来配对的。

    13, 重要结构体解析

    (1)cdev结构体
    在linux2.6内核中,使用cdev结构体描述一个字符设备,定义如下:

    struct cdev{
        struct kobject kobj;//内嵌的kobject对象
        struct module *owner;//所属模块
        struct file_operations *ops;//文件操作结构体
        struct list_head list;
        dev_t dev;//设备号,长度为32位,其中高12为主设备号,低20位为此设备号
        unsigned int count;
    };

    (2)file_operations结构体
    结构体file_operations在头文件linux/fs.h中定义,用来存储驱动内核模块提供的对设备进行各种操作的函数的指针。这些函数实际会在应用程序进行linux的open(),write(),read(),close()等系统调用时最终被调用。该结构体的每个域都对应着驱动内核模块用来处理某个被请求的事务的函数地址。源代码(2.6.28.7)如下:

    struct file_operations{
        struct module*owner;
        loff_t (*llseek)(struct file*,loff_t,int);
        ssize_t (*read)(struct file*,char__user*,size_t,loff_t*);
        ssize_t (*write)(struct file*,constchar__user*,size_t,loff_t*);
        ssize_t (*aio_read)(struct kiocb*,cons tstruct iovec*,unsigned long,loff_t);
        ssize_t (*aio_write)(struct kiocb*,const struct iovec*,unsigned long,loff_t);
        int (*readdir)(struct file*,void*,filldir_t);
        unsigned int (*poll)(struct file*,struct poll_table_struct*);
        int (*ioctl)(struc inode*,struct file*,unsigned int,unsigned long);
        long (*unlocked_ioctl)(struct file*,unsigned int,unsigned long);
        long (*compat_ioctl)(struct file*,unsigned int,unsigned long);
        int (*mmap)(struct file*,struct vm_area_struct*);
        int (*open)(struct inode*,struct file*);
        int (*flush)(struct file*,fl_owner_t id);
        int (*release)(struct inode*,struct file*);
        int (*fsync)(struct file*,struct dentry*,int datasync);
        int (*aio_fsync)(struct kiocb*,int datasync);
        in (*fasync)(int,struct file*,int);
        int (*lock)(struct file*,int,struct file_lock*);
        ssize_t (*sendpage)(struct file*,struct page*,int,size_t,loff_t*,int);
        unsigned long (*get_unmapped_area)(struct file*,unsigned long,unsigned long,unsigned long,unsigned long);
        in t(*check_flags)(int);
        int (*dir_notify)(structfile*filp,unsignedlongarg);
        int (*flock)(structfile*,int,structfile_lock*);
        ssize_t (*splice_write)(struct pipe_inode_info*,struct file*,loff_t*,size_t,unsig ned int);
        ssize_t (*splice_read)(struct file*,loff_t*,struct pipe_inode_info*,size_t,unsigned int);
        int(*setlease)(struct file*,long,struct file_lock**);
    }; 

    解析:

    struct module*owner;
    /*第一个file_operations成员根本不是一个操作;它是一个指向拥有这个结构的模块的指针.
    这个成员用来在它的操作还在被使用时阻止模块被卸载.几乎所有时间中,它被简单初始化为
    THIS_MODULE,一个在中定义的宏.这个宏比较复杂,在进行简单学习操作的时候,一般初始化为THIS_MODULE。*/
    loff_t (*llseek)(struct file*filp,loff_tp,int orig);
    /*(指针参数filp为进行读取信息的目标文件结构体指针;参数p为文件定位的目标偏移量;参数orig为对文件定位
    的起始地址,这个值可以为文件开头(SEEK_SET,0,当前位置(SEEK_CUR,1),文件末尾(SEEK_END,2))
    llseek方法用作改变文件中的当前读/写位置,并且新位置作为(正的)返回值.
    loff_t参数是一个"longoffset",并且就算在32位平台上也至少64位宽.错误由一个负返回值指示.
    如果这个函数指针是NULL,seek调用会以潜在地无法预知的方式修改file结构中的位置计数器(在"file结构"一节中描述).*/
    ssize_t (*read)(struct file *filp,char__user *buffer,size_t size,loff_t *p);
    /*(指针参数filp为进行读取信息的目标文件,指针参数buffer为对应放置信息的缓冲区(即用户空间内存地址),
    参数size为要读取的信息长度,参数p为读的位置相对于文件开头的偏移,在读取信息后,这个指针一般都会移动,移动的值为要读取信息的长度值)
    这个函数用来从设备中获取数据.在这个位置的一个空指针导致read系统调用以-EINVAL("Invalidargument")失败.
    一个非负返回值代表了成功读取的字节数(返回值是一个"signedsize"类型,常常是目标平台本地的整数类型).*/
    ssize_t (*aio_read)(struct kiocb*,char__user *buffer,size_t size,loff_t p);
    /*可以看出,这个函数的第一、三个参数和本结构体中的read()函数的第一、三个参数是不同的,
    异步读写的第三个参数直接传递值,而同步读写的第三个参数传递的是指针,因为AIO从来不需要改变文件的位置。
    异步读写的第一个参数为指向kiocb结构体的指针,而同步读写的第一参数为指向file结构体的指针,每一个I/O请求都对应一个kiocb结构体);
    初始化一个异步读--可能在函数返回前不结束的读操作.如果这个方法是NULL,所有的操作会由read代替进行(同步地).
    (有关linux异步I/O,可以参考有关的资料,《linux设备驱动开发详解》中给出了详细的解答)*/
    ssize_t (*write)(struct file*filp,const char__user *buffer,size_t count,loff_t *ppos);
    /*(参数filp为目标文件结构体指针,buffer为要写入文件的信息缓冲区,count为要写入信息的长度,
    ppos为当前的偏移位置,这个值通常是用来判断写文件是否越界)
    发送数据给设备.如果NULL,-EINVAL返回给调用write系统调用的程序.如果非负,返回值代表成功写的字节数.
    (注:这个操作和上面的对文件进行读的操作均为阻塞操作)*/
    ssize_t (*aio_write)(struct kiocb*,const char__user *buffer,size_t count,loff_t *ppos);
    /*初始化设备上的一个异步写.参数类型同aio_read()函数;*/
    int (*readdir)(struct file*filp,void*,filldir_t);
    /*对于设备文件这个成员应当为NULL;它用来读取目录,并且仅对文件系统有用.*/
    unsigned int(*poll)(struct file*,struct poll_table_struct*);
    /*(这是一个设备驱动中的轮询函数,第一个参数为file结构指针,第二个为轮询表指针)
    这个函数返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI,POLLERR,POLLNVAL等宏的位“或”结果。
    每个宏都表明设备的一种状态,如:POLLIN(定义为0x0001)意味着设备可以无阻塞的读,POLLOUT(定义为0x0004)意味着设备可以无阻塞的写。
    (poll方法是3个系统调用的后端:poll,epoll,和select,都用作查询对一个或多个文件描述符的读或写是否会阻塞.
    poll方法应当返回一个位掩码指示是否非阻塞的读或写是可能的,并且,可能地,提供给内核信息用来使调用进程睡眠直到I/O变为可能.
    如果一个驱动的poll方法为NULL,设备假定为不阻塞地可读可写.
    (这里通常将设备看作一个文件进行相关的操作,而轮询操作的取值直接关系到设备的响应情况,可以是阻塞操作结果,同时也可以是非阻塞操作结果)*/
    int (*ioctl)(struct inode*inode,struct file*filp,unsigned int cmd,unsigned long arg);
    /*(inode和filp指针是对应应用程序传递的文件描述符fd的值,和传递给open方法的相同参数.
    cmd参数从用户那里不改变地传下来,并且可选的参数arg参数以一个unsignedlong的形式传递,不管它是否由用户给定为一个整数或一个指针.
    如果调用程序不传递第3个参数,被驱动操作收到的arg值是无定义的.
    因为类型检查在这个额外参数上被关闭,编译器不能警告你如果一个无效的参数被传递给ioctl,并且任何关联的错误将难以查找.)
    ioctl系统调用提供了发出设备特定命令的方法(例如格式化软盘的一个磁道,这不是读也不是写).另外,几个ioctl命令被内核识别而不必引用fops表.
    如果设备不提供ioctl方法,对于任何未事先定义的请求(-ENOTTY,"设备无这样的ioctl"),系统调用返回一个错误.*/
    int(*mmap)(struct file*,struct vm_area_struct*);
    /*mmap用来请求将设备内存映射到进程的地址空间.如果这个方法是NULL,mmap系统调用返回-ENODEV.
    (如果想对这个函数有个彻底的了解,那么请看有关“进程地址空间”介绍的书籍)*/
    int(*open)(struct inode *inode,struct file *filp);
    /*(inode为文件节点,这个节点只有一个,无论用户打开多少个文件,都只是对应着一个inode结构;
    但是filp就不同,只要打开一个文件,就对应着一个file结构体,file结构体通常用来追踪文件在运行时的状态信息)
    尽管这常常是对设备文件进行的第一个操作,不要求驱动声明一个对应的方法.如果这个项是NULL,设备打开一直成功,但是你的驱动不会得到通知.
    与open()函数对应的是release()函数。*/
    int(*flush)(struct file*);
    /*flush操作在进程关闭它的设备文件描述符的拷贝时调用;它应当执行(并且等待)设备的任何未完成的操作.
    这个必须不要和用户查询请求的fsync操作混淆了.当前,flush在很少驱动中使用;
    SCSI磁带驱动使用它,例如,为确保所有写的数据在设备关闭前写到磁带上.如果flush为NULL,内核简单地忽略用户应用程序的请求.*/
    int(*release)(struct inode*,struct file*);
    /*release()函数当最后一个打开设备的用户进程执行close()系统调用的时候,内核将调用驱动程序release()函数:
    void release(struct inode inode,struct file *file),release函数的主要任务是清理未结束的输入输出操作,释放资源,用户自定义排他标志的复位等。
    在文件结构被释放时引用这个操作.如同open,release可以为NULL.*/
    int (*synch)(struct file*,struct dentry*,intdatasync);
    //刷新待处理的数据,允许进程把所有的脏缓冲区刷新到磁盘。
    int(*aio_fsync)(struct kiocb*,int);
    /*这是fsync方法的异步版本.所谓的fsync方法是一个系统调用函数。系统调用fsync
    把文件所指定的文件的所有脏缓冲区写到磁盘中(如果需要,还包括存有索引节点的缓冲区)。
    相应的服务例程获得文件对象的地址,并随后调用fsync方法。通常这个方法以调用函数__writeback_single_inode()结束,
    这个函数把与被选中的索引节点相关的脏页和索引节点本身都写回磁盘。*/
    int(*fasync)(int,struct file*,int);
    //这个函数是系统支持异步通知的设备驱动,下面是这个函数的模板:
    static int***_fasync(intfd,structfile*filp,intmode)
    {
    struct***_dev*dev=filp->private_data;
    returnfasync_helper(fd,filp,mode,&dev->async_queue);//第四个参数为fasync_struct结构体指针的指针。
    //这个函数是用来处理FASYNC标志的函数。(FASYNC:表示兼容BSD的fcntl同步操作)当这个标志改变时,驱动程序中的fasync()函数将得到执行。
    }
    /*此操作用来通知设备它的FASYNC标志的改变.异步通知是一个高级的主题,在第6章中描述.
    这个成员可以是NULL如果驱动不支持异步通知.*/
    int (*lock)(struct file*,int,struct file_lock*);
    //lock方法用来实现文件加锁;加锁对常规文件是必不可少的特性,但是设备驱动几乎从不实现它.
    ssize_t (*readv)(structfile*,const struct iovec*,unsigned long,loff_t*);
    ssize_t (*writev)(struct file*,const struct iovec*,unsigned long,loff_t*);
    /*这些方法实现发散/汇聚读和写操作.应用程序偶尔需要做一个包含多个内存区的单个读或写操作;
    这些系统调用允许它们这样做而不必对数据进行额外拷贝.如果这些函数指针为NULL,read和write方法被调用(可能多于一次).*/
    ssize_t (*sendfile)(struct file*,loff_t*,size_t,read_actor_t,void*);
    /*这个方法实现sendfile系统调用的读,使用最少的拷贝从一个文件描述符搬移数据到另一个.
    例如,它被一个需要发送文件内容到一个网络连接的web服务器使用.设备驱动常常使sendfile为NULL.*/
    ssize_t (*sendpage)(structfile*,structpage*,int,size_t,loff_t*,int);
    /*sendpage是sendfile的另一半;它由内核调用来发送数据,一次一页,到对应的文件.设备驱动实际上不实现sendpage.*/
    unsigned long(*get_unmapped_area)(struct file*,unsigned long,unsignedlong,unsigned long,unsigned long);
    /*这个方法的目的是在进程的地址空间找一个合适的位置来映射在底层设备上的内存段中.
    这个任务通常由内存管理代码进行;这个方法存在为了使驱动能强制特殊设备可能有的任何的对齐请求.大部分驱动可以置这个方法为NULL.[10]*/
    int (*check_flags)(int)
    //这个方法允许模块检查传递给fnctl(F_SETFL...)调用的标志.
    int (*dir_notify)(struct file*,unsigned long);
    //这个方法在应用程序使用fcntl来请求目录改变通知时调用.只对文件系统有用;驱动不需要实现dir_notify.

    14, 字符设备驱动程序设计基础

    主设备号和次设备号(二者一起为设备号):
    一个字符设备或块设备都有一个主设备号和一个次设备号。主设备号用来标识与设备文件相连的驱动程序,用来反映设备类型。次设备号被驱动程序用来辨别操作的是哪个设备,用来区分同类型的设备。
    linux内核中,设备号用dev_t来描述,2.6.28中定义如下:

    typedef u_long dev_t;

    在32位机中是4个字节,高12位表示主设备号,低12位表示次设备号。

    可以使用下列宏从dev_t中获得主次设备号:也可以使用下列宏通过主次设备号生成dev_t:

    MAJOR(dev_tdev);
    MKDEV(intmajor,intminor);
    MINOR(dev_tdev);

    分配设备号(两种方法):
    (1)静态申请:

    int register_chrdev_region(dev_t from,unsigned count,const char *name);
    

    (2)动态分配:

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

    注销设备号:

    void unregister_chrdev_region(dev_t from,unsigned count);
    

    创建设备文件:
    利用cat/proc/devices查看申请到的设备名,设备号。
    (1)使用mknod手工创建:mknod filename type major minor
    (2)自动创建;
    利用udev(mdev)来实现设备文件的自动创建,首先应保证支持udev(mdev),由busybox配置。在驱动初始化代码里调用class_create为该设备创建一个class,再为每个设备调用device_create创建对应的设备。

    15, 字符设备驱动程序设计

    设备注册:
    字符设备的注册分为三个步骤:
    (1)分配

    cdev:struct cdev *cdev_alloc(void);
    

    (2)初始化

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

    (3)添加

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

    设备操作的实现:
    file_operations函数集的实现。

    struct file_operations xxx_ops={
    .owner=THIS_MODULE,
    .llseek=xxx_llseek,
    .read=xxx_read,
    .write=xxx_write,
    .ioctl=xxx_ioctl,
    .open=xxx_open,
    .release=xxx_release,
    …
    };

    特别注意:驱动程序应用程序的数据交换:
    驱动程序和应用程序的数据交换是非常重要的。file_operations中的read()和write()函数,就是用来在驱动程序和应用程序间交换数据的。通过数据交换,驱动程序和应用程序可以彼此了解对方的情况。但是驱动程序和应用程序属于不同的地址空间。驱动程序不能直接访问应用程序的地址空间;同样应用程序也不能直接访问驱动程序的地址空间,否则会破坏彼此空间中的数据,从而造成系统崩溃,或者数据损坏。安全的方法是使用内核提供的专用函数,完成数据在应用程序空间和驱动程序空间的交换。这些函数对用户程序传过来的指针进行了严格的检查和必要的转换,从而保证用户程序与驱动程序交换数据的安全性。这些函数有:

    unsigned long copy_to_user(void__user *to,const void *from,unsigned long n);
    unsigned long copy_from_user(void *to,constvoid __user *from,unsigned long n);
    put_user(local,user);
    get_user(local,user);

    设备注销:

    void cdev_del(struct cdev *p);
    

    16,ioctl函数说明

    ioctl是设备驱动程序中对设备的I/O通道进行管理的函数。所谓对I/O通道进行管理,就是对设备的一些特性进行控制,例如串口的传输波特率、马达的转速等等。它的调用个数如下:

    int ioctl(int fd,ind cmd,…);
    

    其中fd就是用户程序打开设备时使用open函数返回的文件标示符,cmd就是用户程序对设备的控制命令,后面的省略号是一些补充参数,有或没有是和cmd的意义相关的。
    ioctl函数是文件结构中的一个属性分量,就是说如果你的驱动程序提供了对ioctl的支持,用户就可以在用户程序中使用ioctl函数控制设备的I/O通道。

    命令的组织是有一些讲究的,因为我们一定要做到命令和设备是一一对应的,这样才不会将正确的命令发给错误的设备,或者是把错误的命令发给正确的设备,或者是把错误的命令发给错误的设备。
    所以在Linux核心中是这样定义一个命令码的:

    设备类型 序列号 方向 数据尺寸
    8bit 8bit 2bit 13~14bit

    这样一来,一个命令就变成了一个整数形式的命令码。但是命令码非常的不直观,所以LinuxKernel中提供了一些宏,这些宏可根据便于理解的字符串生成命令码,或者是从命令码得到一些用户可以理解的字符串以标明这个命令对应的设备类型、设备序列号、数据传送方向和数据传输尺寸。
    点击(此处)折叠或打开

    /*used to create numbers*/
    #define _IO(type,nr)        _IOC(_IOC_NONE,(type),(nr),0)
    #define _IOR(type,nr,size)    _IOC(_IOC_READ,(type),(nr),(_IOC_TYPECHECK(size)))
    #define _IOW(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
    #define _IOWR(type,nr,size)    _IOC(_IOC_READ|_IOC_WRITE,(type),(nr),(_IOC_TYPECHECK(size)))
    #defin e_IOR_BAD(type,nr,size)    _IOC(_IOC_READ,(type),(nr),sizeof(size))
    #define _IOW_BAD(type,nr,size)    _IOC(_IOC_WRITE,(type),(nr),sizeof(size))
    #define _IOWR_BAD(type,nr,size)_IOC(_IOC_READ|_IOC_WRITE,(type),(nr),sizeof(size))
    
    #define _IOC(dir,type,nr,size)\
        (((dir)<<_IOC_DIRSHIFT)|\
        ((type)<<_IOC_TYPESHIFT)|\
        ((nr)<<_IOC_NRSHIFT)|\
        ((size)<<_IOC_SIZESHIFT))

    17,文件私有数据

    大多数linux的驱动工程师都将文件私有数据private_data指向设备结构体,read等个函数通过调用private_data来访问设备结构体。这样做的目的是为了区分子设备,如果一个驱动有两个子设备(次设备号分别为0和1),那么使用private_data就很方便。
    这里有一个函数要提出来:

    container_of(ptr,type,member)//通过结构体成员的指针找到对应结构体的的指针
    

    其定义如下:

    /**
    *container_of-castamemberofastructureouttothecontainingstructure
    *@ptr:    thepointertothemember.
    *@type:    thetypeofthecontainerstructthisisembeddedin.
    *@member:    thenameofthememberwithinthestruct.
    *
    */
    #define container_of(ptr,type,member)({            \
        const typeof(((type*)0)->member)*__mptr=(ptr);    \
        (type*)((char*)__mptr-offsetof(type,member));})

    18,字符设备驱动的结构

    可以概括如下图:
    这里写图片描述
    字符设备是3大类设备(字符设备、块设备、网络设备)中较简单的一类设备,其驱动程序中完成的主要工作是初始化、添加和删除cdev结构体,申请和释放设备号,以及填充file_operation结构体中操作函数,并实现file_operations结构体中的read()write()ioctl()等重要函数。如图所示为cdev结构体、file_operations和用户空间调用驱动的关系。

    19, 自旋锁与信号量

    为了避免并发,防止竞争。内核提供了一组同步方法来提供对共享数据的保护。我们的重点不是介绍这些方法的详细用法,而是强调为什么使用这些方法和它们之间的差别。
    Linux使用的同步机制可以说从2.0到2.6以来不断发展完善。从最初的原子操作,到后来的信号量,从大内核锁到今天的自旋锁。这些同步机制的发展伴随Linux从单处理器到对称多处理器的过度;伴随着从非抢占内核到抢占内核的过度。锁机制越来越有效,也越来越复杂。目前来说内核中原子操作多用来做计数使用,其它情况最常用的是两种锁以及它们的变种:一个是自旋锁,另一个是信号量。

    自旋锁
    自旋锁是专为防止多处理器并发而引入的一种锁,它在内核中大量应用于中断处理等部分(对于单处理器来说,防止中断处理中的并发可简单采用关闭中断的方式,不需要自旋锁)。
    自旋锁最多只能被一个内核任务持有,如果一个内核任务试图请求一个已被争用(已经被持有)的自旋锁,那么这个任务就会一直进行忙循环——旋转——等待锁重新可用。要是锁未被争用,请求它的内核任务便能立刻得到它并且继续进行。自旋锁可以在任何时刻防止多于一个的内核任务同时进入临界区,因此这种锁可有效地避免多处理器上并发运行的内核任务竞争共享资源。

    自旋锁的基本形式如下:
    spin_lock(&mr_lock);
    //临界区
    spin_unlock(&mr_lock);

    信号量
    Linux中的信号量是一种睡眠锁。如果有一个任务试图获得一个已被持有的信号量时,信号量会将其推入等待队列,然后让其睡眠。这时处理器获得自由去执行其它代码。当持有信号量的进程将信号量释放后,在等待队列中的一个任务将被唤醒,从而便可以获得这个信号量。
    信号量的睡眠特性,使得信号量适用于锁会被长时间持有的情况;只能在进程上下文中使用,因为中断上下文中是不能被调度的;另外当代码持有信号量时,不可以再持有自旋锁。
    信号量基本使用形式为:

    static DECLARE_MUTEX(mr_sem);//声明互斥信号量
    if(down_interruptible(&mr_sem))
    //可被中断的睡眠,当信号来到,睡眠的任务被唤醒
    //临界区
    up(&mr_sem);

    信号量和自旋锁区别
    从严格意义上说,信号量和自旋锁属于不同层次的互斥手段,前者的实现有赖于后者,在信号量本身的实现上,为了保证信号量结构存取的原子性,在多CPU中需要自旋锁来互斥。
    信号量是进程级的。用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺进程。鉴于进程上下文切换的开销也很大,因此,只有当进程占用资源时间比较长时,用信号量才是较好的选择。
    当所要保护的临界区访问时间比较短时,用自旋锁是非常方便的,因为它节省上下文切换的时间,但是CPU得不到自旋锁会在那里空转直到执行单元锁为止,所以要求锁不能在临界区里长时间停留,否则会降低系统的效率
    由此,可以总结出自旋锁和信号量选用的3个原则:
    1:当锁不能获取到时,使用信号量的开销就是进程上线文切换的时间Tc,使用自旋锁的开销就是等待自旋锁(由临界区执行的时间决定)Ts,如果Ts比较小时,应使用自旋锁比较好,如果Ts比较大,应使用信号量。
    2:信号量所保护的临界区可包含可能引起阻塞的代码,而自旋锁绝对要避免用来保护包含这样的代码的临界区,因为阻塞意味着要进行进程间的切换,如果进程被切换出去后,另一个进程企图获取本自旋锁,死锁就会发生。
    3:信号量存在于进程上下文,因此,如果被保护的共享资源需要在中断或软中断情况下使用,则在信号量和自旋锁之间只能选择自旋锁,当然,如果一定要是要那个信号量,则只能通过down_trylock()方式进行,不能获得就立即返回以避免阻塞
    自旋锁VS信号量
    需求建议的加锁方法
    低开销加锁优先使用自旋锁
    短期锁定优先使用自旋锁
    长期加锁优先使用信号量
    中断上下文中加锁使用自旋锁
    持有锁是需要睡眠、调度使用信号量

    20, 阻塞与非阻塞I/O

    一个驱动当它无法立刻满足请求应当如何响应?一个对 read 的调用可能当没有数据时到来,而以后会期待更多的数据;或者一个进程可能试图写,但是你的设备没有准备好接受数据,因为你的输出缓冲满了。调用进程往往不关心这种问题,程序员只希望调用 read 或 write 并且使调用返回,在必要的工作已完成后,你的驱动应当(缺省地)阻塞进程,使它进入睡眠直到请求可继续。
    阻塞操作是指在执行设备操作时若不能获得资源则挂起进程,直到满足可操作的条件后再进行操作。
    一个典型的能同时处理阻塞与非阻塞的globalfifo读函数如下:

    /*globalfifo读函数*/
    static ssize_t globalfifo_read(struct file *filp, char __user *buf, size_t count,
        loff_t *ppos)
    {
        int ret;
        struct globalfifo_dev *dev = filp->private_data;
        DECLARE_WAITQUEUE(wait, current);
    
        down(&dev->sem); /* 获得信号量 */
        add_wait_queue(&dev->r_wait, &wait); /* 进入读等待队列头 */
    
        /* 等待FIFO非空 */
        if (dev->current_len == 0) {
            if (filp->f_flags &O_NONBLOCK) {
                ret = - EAGAIN;
                goto out;
            }
            __set_current_state(TASK_INTERRUPTIBLE); /* 改变进程状态为睡眠 */
            up(&dev->sem);
    
            schedule(); /* 调度其他进程执行 */
            if (signal_pending(current)) {
                /* 如果是因为信号唤醒 */
                ret = - ERESTARTSYS;
                goto out2;
            }
    
            down(&dev->sem);
        }
    
        /* 拷贝到用户空间 */
        if (count > dev->current_len)
            count = dev->current_len;
    
        if (copy_to_user(buf, dev->mem, count)) {
            ret = - EFAULT;
            goto out;
        } else {
            memcpy(dev->mem, dev->mem + count, dev->current_len - count); /* fifo数据前移 */
            dev->current_len -= count; /* 有效数据长度减少 */
            printk(KERN_INFO "read %d bytes(s),current_len:%d\n", count, dev->current_len);
    
            wake_up_interruptible(&dev->w_wait); /* 唤醒写等待队列 */
    
            ret = count;
        }
    out:
        up(&dev->sem); /* 释放信号量 */
    out2:
        remove_wait_queue(&dev->w_wait, &wait); /* 从附属的等待队列头移除 */
        set_current_state(TASK_RUNNING);
        return ret;
    }
    

    21, poll方法

    使用非阻塞I/O的应用程序通常会使用select()poll()系统调用查询是否可对设备进行无阻塞的访问。select()poll()系统调用最终会引发设备驱动中的poll()函数被执行。
    这个方法由下列的原型:

    unsigned int (*poll) (struct file *filp, poll_table *wait);
    

    这个驱动方法被调用, 无论何时用户空间程序进行一个 poll, select, 或者 epoll 系统调用, 涉及一个和驱动相关的文件描述符. 这个设备方法负责这 2 步:

    1. 对可能引起设备文件状态变化的等待队列,调用poll_wait()函数,将对应的等待队列头添加到poll_table.
    2. 返回一个位掩码, 描述可能不必阻塞就立刻进行的操作.

    poll_table结构, 给 poll 方法的第 2 个参数, 在内核中用来实现 poll, select, 和 epoll 调用; 它在 中声明, 这个文件必须被驱动源码包含. 驱动编写者不必要知道所有它内容并且必须作为一个不透明的对象使用它; 它被传递给驱动方法以便驱动可用每个能唤醒进程的等待队列来加载它, 并且可改变 poll 操作状态. 驱动增加一个等待队列到poll_table结构通过调用函数 poll_wait:

    void poll_wait (struct file *, wait_queue_head_t *, poll_table *);
    

    poll 方法的第 2 个任务是返回位掩码, 它描述哪个操作可马上被实现; 这也是直接的. 例如, 如果设备有数据可用, 一个读可能不必睡眠而完成; poll 方法应当指示这个时间状态. 几个标志(通过 定义)用来指示可能的操作:
    POLLIN:如果设备可被不阻塞地读, 这个位必须设置.
    POLLRDNORM:这个位必须设置, 如果”正常”数据可用来读. 一个可读的设备返回( POLLIN|POLLRDNORM ).
    POLLOUT:这个位在返回值中设置, 如果设备可被写入而不阻塞.
    ……
    poll的一个典型模板如下:

    static unsigned int globalfifo_poll(struct file *filp, poll_table *wait)
    {
        unsigned int mask = 0;
        struct globalfifo_dev *dev = filp->private_data; /*获得设备结构体指针*/
    
        down(&dev->sem);
    
        poll_wait(filp, &dev->r_wait, wait);
        poll_wait(filp, &dev->w_wait, wait);
        /*fifo非空*/
        if (dev->current_len != 0) {
            mask |= POLLIN | POLLRDNORM; /*标示数据可获得*/
        }
        /*fifo非满*/
        if (dev->current_len != GLOBALFIFO_SIZE) {
            mask |= POLLOUT | POLLWRNORM; /*标示数据可写入*/
        }
    
        up(&dev->sem);
        return mask;
    }

    应用程序如何去使用这个poll呢?一般用select()来实现,其原型为:

    int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
    

    其中,readfds, writefds, exceptfds,分别是被select()监视的读、写和异常处理的文件描述符集合。numfds是需要检查的号码最高的文件描述符加1。
    以下是一个具体的例子:

    /*======================================================================
        A test program in userspace 
        This example is to introduce the ways to use "select"
         and driver poll 
    
        The initial developer of the original code is Baohua Song
        <author@linuxdriver.cn>. All Rights Reserved.
    ======================================================================*/
    #include <sys/types.h>
    #include <sys/stat.h>
    #include <stdio.h>
    #include <fcntl.h>
    #include <unistd.h>
    #include <sys/time.h>
    
    #define FIFO_CLEAR 0x1
    #define BUFFER_LEN 20
    main()
    {
      int fd, num;
      char rd_ch[BUFFER_LEN];
      fd_set rfds,wfds;
    
      /*以非阻塞方式打开/dev/globalmem设备文件*/
      fd = open("/dev/globalfifo", O_RDONLY | O_NONBLOCK);
      if (fd != - 1)
      {
        /*FIFO清0*/
        if (ioctl(fd, FIFO_CLEAR, 0) < 0)
        {
          printf("ioctl command failed\n");
        }
        while (1)
        {
          FD_ZERO(&rfds);// 清除一个文件描述符集rfds
          FD_ZERO(&wfds); 
          FD_SET(fd, &rfds);// 将一个文件描述符fd,加入到文件描述符集rfds中
          FD_SET(fd, &wfds);
    
          select(fd + 1, &rfds, &wfds, NULL, NULL);
          /*数据可获得*/
          if (FD_ISSET(fd, &rfds)) //判断文件描述符fd是否被置位
          {
              printf("Poll monitor:can be read\n");
          }
          /*数据可写入*/
          if (FD_ISSET(fd, &wfds))
          {
              printf("Poll monitor:can be written\n");
          } 
        }
      }
      else
      {
        printf("Device open failure\n");
      }
    }

    其中:
    FD_ZERO(fd_set *set); //清除一个文件描述符集set
    FD_SET(int fd, fd_set *set); //将一个文件描述符fd,加入到文件描述符集set中
    FD_CLEAR(int fd, fd_set *set); //将一个文件描述符fd,从文件描述符集set中清除
    FD_ISSET(int fd, fd_set *set); //判断文件描述符fd是否被置位。

    22,并发与竞态介绍

    Linux设备驱动中必须解决一个问题是多个进程对共享资源的并发访问,并发的访问会导致竞态,在当今的Linux内核中,支持SMP与内核抢占的环境下,更是充满了并发与竞态。幸运的是,Linux 提供了多钟解决竞态问题的方式,这些方式适合不同的应用场景。例如:中断屏蔽、原子操作、自旋锁、信号量等等并发控制机制。
    并发与竞态的概念
    并发是指多个执行单元同时、并发被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态。
    临界区概念是为解决竞态条件问题而产生的,一个临界区是一个不允许多路访问的受保护的代码,这段代码可以操纵共享数据或共享服务。临界区操纵坚持互斥锁原则(当一个线程处于临界区中,其他所有线程都不能进入临界区)。然而,临界区中需要解决的一个问题是死锁。

    23, 中断屏蔽

    在单CPU 范围内避免竞态的一种简单而省事的方法是进入临界区之前屏蔽系统的中断。CPU 一般都具有屏蔽中断和打开中断的功能,这个功能可以保证正在执行的内核执行路径不被中断处理程序所抢占,有效的防止了某些竞态条件的发送,总之,中断屏蔽将使得中断与进程之间的并发不再发生。
    中断屏蔽的使用方法:

    local_irq_disable() /屏蔽本地CPU 中断/ 
    ….. 
    critical section /临界区受保护的数据/ 
    ….. 
    local_irq_enable() /打开本地CPU 中断/ 

    由于Linux 的异步I/O、进程调度等很多重要操作都依赖于中断,中断对内核的运行非常重要,在屏蔽中断期间的所有中断都无法得到处理,因此长时间屏蔽中断是非常危险的,有可能造成数据的丢失,甚至系统崩溃的后果。这就要求在屏蔽了中断后,当前的内核执行路径要尽快地执行完临界区代码。
    local_irq_disable()不同的是,local_irq_save(flags)除了进行禁止中断的操作外,还保存当前CPU 的中断状态位信息;与local_irq_enable()不同的是,local_irq_restore(flags) 除了打开中断的操作外,还恢复了CPU 被打断前的中断状态位信息。

    24, 原子操作

    原子操作指的是在执行过程中不会被别的代码路径所中断的操作,Linux 内核提供了两类原子操作——位原子操作和整型原子操作。它们的共同点是在任何情况下都是原子的,内核代码可以安全地调用它们而不被打断。然而,位和整型变量原子操作都依赖于底层CPU 的原子操作来实现,因此这些函数的实现都与 CPU 架构密切相关。
    1 整型原子操作
    1)、设置原子变量的值

    void atomic_set(atomic v,int i); /设置原子变量的值为 i */ 
    atomic_t v = ATOMIC_INIT(0); /定义原子变量 v 并初始化为 0 / 

    2)、获取原子变量的值

    int atomic_read(atomic_t v) /返回原子变量 v 的当前值*/ 
    

    3)、原子变量加/减

    void atomic_add(int i,atomic_t v) /原子变量增加 i */ 
    void atomic_sub(int i,atomic_t v) /原子变量减少 i */ 
    

    4)、原子变量自增/自减

    void atomic_inc(atomic_t v) /原子变量增加 1 */ 
    void atomic_dec(atomic_t v) /原子变量减少 1 */ 
    

    5)、操作并测试

    int atomic_inc_and_test(atomic_t *v); 
    int atomic_dec_and_test(atomic_t *v); 
    int atomic_sub_and_test(int i, atomic_t *v); 
    

    上述操作对原子变量执行自增、自减和减操作后测试其是否为 0 ,若为 0 返回true,否则返回false。注意:没有atomic_add_and_test(int i, atomic_t *v)
    6)、操作并返回

    int atomic_add_return(int i, atomic_t *v); 
    int atomic_sub_return(int i, atomic_t *v); 
    int atomic_inc_return(atomic_t *v); 
    int atomic_dec_return(atomic_t *v); 
    

    上述操作对原子变量进行加/减和自增/自减操作,并返回新的值。
    2 位原子操作
    1)、设置位

    void set_bit(nr,void addr);/设置addr 指向的数据项的第 nr 位为1 */ 
    

    2)、清除位

    void clear_bit(nr,void addr)/设置addr 指向的数据项的第 nr 位为0 */ 
    

    3)、取反位

    void change_bit(nr,void addr); /对addr 指向的数据项的第 nr 位取反操作*/ 
    

    4)、测试位

    test_bit(nr,void addr);/返回addr 指向的数据项的第 nr位*/ 
    

    5)、测试并操作位

    int test_and_set_bit(nr, void *addr); 
    int test_and_clear_bit(nr,void *addr); 
    int test_amd_change_bit(nr,void *addr); 
    

    25, 自旋锁

    自旋锁(spin lock)是一种典型的对临界资源进行互斥访问的手段。为了获得一个自旋锁,在某CPU 上运行的代码需先执行一个原子操作,该操作测试并设置某个内存变量,由于它是原子操作,所以在该操作完成之前其他执行单元不能访问这个内存变量。如果测试结果表明锁已经空闲,则程序获得这个自旋锁并继续执行;如果测试结果表明锁仍被占用,则程序将在一个小的循环里面重复这个“测试并设置” 操作,即进行所谓的“自旋”。
    理解自旋锁最简单的方法是把它当做一个变量看待,该变量把一个临界区标记为“我在这运行了,你们都稍等一会”,或者标记为“我当前不在运行,可以被使用”。
    Linux中与自旋锁相关操作有:
    1)、定义自旋锁

    spinlock_t my_lock; 
    

    2)、初始化自旋锁

    spinlock_t my_lock = SPIN_LOCK_UNLOCKED; /静态初始化自旋锁/ 
    void spin_lock_init(spinlock_t lock); /动态初始化自旋锁*/ 
    

    3)、获取自旋锁

    /若获得锁立刻返回真,否则自旋在那里直到该锁保持者释放/ 
    void spin_lock(spinlock_t *lock); 
    /若获得锁立刻返回真,否则立刻返回假,并不会自旋等待/ 
    void spin_trylock(spinlock_t *lock) 
    

    4)、释放自旋锁

    void spin_unlock(spinlock_t *lock) 
    

    自旋锁的一般用法:

    spinlock_t lock; /定义一个自旋锁/ 
    spin_lock_init(&lock); /动态初始化一个自旋锁/ 
    …… 
    spin_lock(&lock); /获取自旋锁,保护临界区/ 
    ……./临界区/ 
    spin_unlock(&lock); /解锁/ 

    自旋锁主要针对SMP 或单CPU 但内核可抢占的情况,对于单CPU 且内核不支持抢占的系统,自旋锁退化为空操作。尽管用了自旋锁可以保证临界区不受别的CPU和本地CPU内的抢占进程打扰,但是得到锁的代码路径在执行临界区的时候,还可能受到中断和底半部(BH)的影响,为了防止这种影响,就需要用到自旋锁的衍生。
    获取自旋锁的衍生函数:

    void spin_lock_irq(spinlock_t lock); /获取自旋锁之前禁止中断*/ 
    void spin_lock_irqsave(spinlock_t lock, unsigned long flags);/获取自旋锁之前禁止中断,并且将先前的中断状态保存在flags 中*/ 
    void spin_lock_bh(spinlock_t lock); /在获取锁之前禁止软中断,但不禁止硬件中断*/ 

    释放自旋锁的衍生函数:

    void spin_unlock_irq(spinlock_t *lock) 
    void spin_unlock_irqrestore(spinlock_t *lock,unsigned long flags); 
    void spin_unlock_bh(spinlock_t *lock); 

    解锁的时候注意要一一对应去解锁。
    自旋锁注意点:
    (1)自旋锁实际上是忙等待,因此,只有占用锁的时间极短的情况下,使用自旋锁才是合理的。
    (2)自旋锁可能导致系统死锁。
    (3)自旋锁锁定期间不能调用可能引起调度的函数。如:copy_from_user()、copy_to_user()、kmalloc()、msleep()等函数。
    (4)拥有自旋锁的代码是不能休眠的。

    26, 读写自旋锁

    它允许多个读进程并发执行,但是只允许一个写进程执行临界区代码,而且读写也是不能同时进行的。
    1)、定义和初始化读写自旋锁

    rwlock_t my_rwlock = RW_LOCK_UNLOCKED; /* 静态初始化 */ 
    rwlock_t my_rwlock; 
    rwlock_init(&my_rwlock); /* 动态初始化 */ 
    

    2)、读锁定

    void read_lock(rwlock_t *lock); 
    void read_lock_irqsave(rwlock_t *lock, unsigned long flags); 
    void read_lock_irq(rwlock_t *lock); 
    void read_lock_bh(rwlock_t *lock); 

    3)、读解锁

    void read_unlock(rwlock_t *lock); 
    void read_unlock_irqrestore(rwlock_t *lock, unsigned long flags); 
    void read_unlock_irq(rwlock_t *lock); 
    void read_unlock_bh(rwlock_t *lock); 

    在对共享资源进行读取之前,应该先调用读锁定函数,完成之后调用读解锁函数。
    4)、写锁定

    void write_lock(rwlock_t *lock); 
    void write_lock_irqsave(rwlock_t *lock, unsigned long flags); 
    void write_lock_irq(rwlock_t *lock); 
    void write_lock_bh(rwlock_t *lock); 
    void write_trylock(rwlock_t *lock); 

    5)、写解锁

    void write_unlock(rwlock_t *lock); 
    void write_unlock_irqrestore(rwlock_t *lock, unsigned long flags); 
    void write_unlock_irq(rwlock_t *lock); 
    void write_unlock_bh(rwlock_t *lock); 

    在对共享资源进行写之前,应该先调用写锁定函数,完成之后应调用写解锁函数。

    读写自旋锁的一般用法:

    rwlock_t lock; /定义一个读写自旋锁 rwlock/ 
    rwlock_init(&lock); /初始化/ 
    read_lock(&lock); /读取前先获取锁/ 
    …../临界区资源/ 
    read_unlock(&lock); /读完后解锁/ 
    write_lock_irqsave(&lock, flags); /写前先获取锁/ 
    …../临界区资源/ 
    write_unlock_irqrestore(&lock,flags); /写完后解锁/ 

    27, 顺序锁(sequence lock)

    顺序锁是对读写锁的一种优化,读执行单元在写执行单元对被顺序锁保护的资源进行写操作时仍然可以继续读,而不必等地写执行单元完成写操作,写执行单元也不必等待所有读执行单元完成读操作才进去写操作。但是,写执行单元与写执行单元依然是互斥的。并且,在读执行单元读操作期间,写执行单元已经发生了写操作,那么读执行单元必须进行重读操作,以便确保读取的数据是完整的,这种锁对于读写同时进行概率比较小的情况,性能是非常好的。
    顺序锁有个限制,它必须要求被保护的共享资源不包含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,就会导致oops。
    1)、初始化顺序锁

    seqlock_t lock1 = SEQLOCK_UNLOCKED; /静态初始化/ 
    seqlock lock2; /动态初始化/ 
    seqlock_init(&lock2) 

    2)、获取顺序锁

    void write_seqlock(seqlock_t *s1); 
    void write_seqlock_irqsave(seqlock_t *lock, unsigned long flags) 
    void write_seqlock_irq(seqlock_t *lock); 
    void write_seqlock_bh(seqlock_t *lock); 
    int write_tryseqlock(seqlock_t *s1); 

    3)、释放顺序锁

    void write_sequnlock(seqlock_t *s1); 
    void write_sequnlock_irqsave(seqlock_t *lock, unsigned long flags) 
    void write_sequnlock_irq(seqlock_t *lock); 
    void write_sequnlock_bh(seqlock_t *lock); 

    写执行单元使用顺序锁的模式如下:

    write_seqlock(&seqlock_a); 
    /写操作代码/ 
    …….. 
    write_sequnlock(&seqlock_a); 

    4)、读开始

    unsigned read_seqbegin(const seqlock_t *s1); 
    unsigned read_seqbegin_irqsave(seqlock_t *lock, unsigned long flags); 

    5)、重读

    int read_seqretry(const seqlock_t *s1, unsigned iv); 
    int read_seqretry_irqrestore(seqlock_t *lock,unsigned int seq,unsigned long flags); 

    读执行单元使用顺序锁的模式如下:

    unsigned int seq; 
    do{ 
    seq = read_seqbegin(&seqlock_a); 
    /读操作代码/ 
    ……. 
    }while (read_seqretry(&seqlock_a, seq)); 

    28, 信号量

    信号量的使用
    信号量(semaphore)是用于保护临界区的一种最常用的办法,它的使用方法与自旋锁是类似的,但是,与自旋锁不同的是,当获取不到信号量的时候,进程不会自旋而是进入睡眠的等待状态。
    1)、定义信号量

    struct semaphore sem; 
    

    2)、初始化信号量

    void sema_init(struct semaphore sem, int val); /初始化信号量的值为 val */ 
    

    更常用的是下面这二个宏:

    #define init_MUTEX(sem) sema_init(sem, 1) 
    #define init_MUTEX_LOCKED(sem) sem_init(sem, 0) 

    然而,下面这两个宏是定义并初始化信号量的“快捷方式”

    DECLARE_MUTEX(name) /一个称为name信号量变量被初始化为 1 / 
    DECLARE_MUTEX_LOCKED(name) /一个称为name信号量变量被初始化为 0 / 

    3)、获得信号量

    /该函数用于获取信号量,若获取不成功则进入不可中断的睡眠状态/ 
    void down(struct semaphore *sem); 
    /该函数用于获取信号量,若获取不成功则进入可中断的睡眠状态/ 
    void down_interruptible(struct semaphore *sem); 
    /该函数用于获取信号量,若获取不成功立刻返回 -EBUSY/ 
    int down_trylock(struct sempahore *sem); 

    4)、释放信号量

    void up(struct semaphore sem); /释放信号量 sem ,并唤醒等待者*/ 
    

    信号量的一般用法:

    DECLARE_MUTEX(mount_sem); /定义一个信号量mount_sem,并初始化为 1 / 
    down(&mount_sem); /* 获取信号量,保护临界区*/ 
    ….. 
    critical section /临界区/ 
    ….. 
    up(&mount_sem); /释放信号量/ 

    29, 读写信号量

    读写信号量可能引起进程阻塞,但是它允许多个读执行单元同时访问共享资源,但最多只能有一个写执行单元。
    1)、定义和初始化读写信号量

    struct rw_semaphore my_rws; /定义读写信号量/ 
    void init_rwsem(struct rw_semaphore sem); /初始化读写信号量*/ 
    

    2)、读信号量获取

    void down_read(struct rw_semaphore *sem); 
    int down_read_trylock(struct rw_semaphore *sem); 
    

    3)、读信号量释放

    void up_read(struct rw_semaphore *sem); 
    

    4)、写信号量获取

    void down_write(struct rw_semaphore *sem); 
    int down_write_trylock(struct rw_semaphore *sem); 
    

    5)、写信号量释放

    void up_write(struct rw_semaphore *sem); 
    

    30, completion

    完成量(completion)用于一个执行单元等待另外一个执行单元执行完某事。
    1)、定义完成量

    struct completion my_completion; 
    

    2)、初始化完成量

    init_completion(&my_completion); 
    

    3)、定义并初始化的“快捷方式”

    DECLARE_COMPLETION(my_completion) 
    

    4)、等待完成量

    void wait_for_completion(struct completion c); /等待一个 completion 被唤醒*/ 
    

    5)、唤醒完成量

    void complete(struct completion c); /只唤醒一个等待执行单元*/ 
    void complete(struct completion c); /唤醒全部等待执行单元*/ 
    

    31, 自旋锁VS信号量

    信号量是进程级的,用于多个进程之间对资源的互斥,虽然也是在内核中,但是该内核执行路径是以进程的身份,代表进程来争夺资源的。如果竞争失败,会发送进程上下文切换,当前进程进入睡眠状态,CPU 将运行其他进程。鉴于开销比较大,只有当进程资源时间较长时,选用信号量才是比较合适的选择。然而,当所要保护的临界区访问时间比较短时,用自旋锁是比较方便的。
    总结:
    解决并发与竞态的方法有(按本文顺序):
    (1)中断屏蔽
    (2)原子操作(包括位和整型原子)
    (3)自旋锁
    (4)读写自旋锁
    (5)顺序锁(读写自旋锁的进化)
    (6)信号量
    (7)读写信号量
    (8)完成量
    其中,中断屏蔽很少单独被使用,原子操作只能针对整数进行,因此自旋锁和信号量应用最为广泛。自旋锁会导致死循环,锁定期间内不允许阻塞,因此要求锁定的临界区小;信号量允许临界区阻塞,可以适用于临界区大的情况。读写自旋锁和读写信号量分别是放宽了条件的自旋锁 信号量,它们允许多个执行单元对共享资源的并发读。

    展开全文
  • Linux设备驱动开发详解》第2版 宋宝华 编著 Bought on Dec 1, 2010, Noted on 2015.6 【声明】本文大部分内容摘自《Linux设备驱动开发详解》第2版,或者网上搜索,故不单独注明内容出处 第一篇 Linux设备驱动...

    《Linux设备驱动开发详解》第2版 宋宝华 编著

    Bought on Dec 1, 2010, Noted on 2015.6

    【声明】本文大部分内容摘自《Linux设备驱动开发详解》第2版,或者网上搜索,故不单独注明内容出处

    第一篇 Linux设备驱动入门

     

    设备的分类

    字符设备;块设备;网络设备

    其中网络设备不会映射到文件系统中的文件和目录,而是面向数据包的接受和发送

     

    处理器分类

    CPU的体系结构:冯诺依曼机构和哈弗结构(程序和数据分开存储,包括独立的总线)

    从指令集角度分:RISC(ARM,MIPS,PowerPC)和CISC(IA x86)

    按应用领域区分:通用处理器(GPP:general-purpose preprocessor),DSP,ASP/ASIC(Application specific integriated circuit)

     

    Linux 2.6内核特点

    1. 新的调度器,在高负载情况下执行极其出色
    2. 内核任务可抢占,提高系统的实时性,使得鼠标和键盘事件得到更快的响应
    3. 改进的线程模型,线程操作速度得以提高,可以处理任意数量的线程
    4. 虚拟内存,增加r-map(方向映射),显著改善虚拟内存在一定程度负载下的性能
    5. 音频。弃用OSS,改用ALSA
     

    Linux Kernel's components

    SCHED(进程调度)MM(内存管理)VFS(虚拟文件系统)NET(网络接口)IPC(进程间通信)
    其中网络接口分为网络协议和网络驱动程序
     

    Coding style

    TAB 8 characters
    if/for one line, without { }
    switch and case aligning
    for (i = 0; i < 10; i++) {
    .......................
    }
     

    GNU C & ANSI C

    GNU 是ANSI C的扩展语法
    1. 零长度和变量长度数组
    char data[0]; ....... then data[i]=......
    int main(int argc, char *argv[])
    {
    int i, n = argc;
    double x[n];
    }
    2. case的范围
    like
    switch (ch) {
    case '0' ... '9': xxx
    case 'a' ... 'f': xxx
    }
    3. typeof 获取变量的type
    e.g. const typeof(x) _x = (x);
    4. 可变参数宏
    e.g. #define pr_debug(fmt, arg...) \
    printk(fmt, ##arg)
    其中##是为了处理参数为零的情况,去除掉fmt后面的逗号
    5. 标号元素
    通过指定索引或者结构体成员名,允许初始化值以任意顺序出现
    struct file_operations ext2_file_operations = {
    llseek: genernic_file_llseek,
    ioctl: ext2_ioctl,
    .....
    }
    6. built-in function in GCC
    For example, __builtin_return_address(LEVEL)
    Checking out GNU GCC manual

    第二篇 Linux设备驱动核心理论

    Linux File System

    System calls of FS

    int create(const char *filename, mode_t mode);
    int umask(int newmask);
    int open(const char *filename, int flags, mode_t mode);
    int read(int fd, const void *buf, size_t length);
    int write(int fd, const void *buf, size_t length);
    int lseek(int fd, offset_t offset, int whence);
    int close(int fd);
     

    File operation API of C library

    dependence on different OS

    FILE *fopen(const char *path, const char *mode);
    int fgetc(FILE *stream);
    int fputc(int c, FILE *stream);
    char *fgets(char *s, int n, FILE *stream);
    int fprintf(FILE *stream, const char *format, ... );
    int fscanf(FILE *steam, const char *format, ... );
    size_t fread(void *ptr, size_t size, size_t n, FILE *stream);
    size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream);
    int fseek(FILE *stream, long offset, int whence);
    int fclose(FILE *stream);
     

    struct file & inode

    file结构体代表一个打开的文件或者设备。
    file->f_mode标识文件的读写权限,file->f_flags标识可反映阻塞和非阻塞

    inode结构体是linux管理文件系统的最基本单位,记录文件各类属性信息,其中i_rdev字段包含设备编号,包含major and minor number。一般major对应驱动,minor对应该驱动的设备序号
     

    sysfs file system

    这个VFS提供了包含所有系统硬件的层级视图,展示设备驱动模型各组件的层次关系,大致分三类:bus,devices,class。
    三个重要结构体分别描述bus,driver,device: bus_type,device_driver,device.
    device and driver's registration is separately. when any one registration, it will try to match another one via match() routine of bus_type();

    Linux Character device

    Main function of cdev

    cdev struct describes character device information.

    MAJOR(dev_t dev)
    MINOR(dev_t dev)
    MKDEV(int major, int minor)

    void cdev_init(struct cdev *, struct file_operations *);
    struct cdev *cdev_alloc(void);

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

    void cdev_put(strcut cdev *p);
    int cdev_add(struct cdev *, dev_t, unsigned);
    void cdev_del(struct cdev*);
     

    procedure of character device initialization

    1. application device number
    2. registration cdev
     

    data transmit

    copy_from_user()
    copy_to_user()
    put_user()
    get_user()
     

    ioctl command format

    Device type + no + direction + size
    (Device type is magic number, see more in ioctl-number.txt)

    _IO()
    _IOR()
    _IOW()
    _IOWR()

     

    private_data


    struct file {
    ....
    void *private_data;
    }
    private_data存储驱动的私有数据,一般在驱动probe时动态开辟内存,以便多个设备共用一个驱动使用;不过随着device——tree的应用,越来越多的驱动数据放在device_tree中。

     

    Concurrency & race condition

    并发和竞态


    critical sections临界资源包含:HW,static/global variables
    竞态发生的情况:SMP,多线程,包括可抢占式内核,中断
    避免竞态的手段:中断屏蔽,原子操作,自旋锁,信号量
     

    中断屏蔽

    可以避免新的中断到来,也可以避免内核抢占的发生(进程调度依赖于中断来实现)

    local_irq_disable()
    local_irq_enable()
    local_irq_save(flags)
    local_irq_restore(flags)
    local_bh_disable() //disable 中断底半部
    local_bh_enable()
     

    原子操作

    For integer operand

    atomic_set
    atomic_read
    atomic_add
    atomic_sub
    atomic_inc_and_test
    atomic_dec_and_test
    atomic_sub_and_test
    atomic_inc_return
    atomic_sub_return
    atomic_dec_return
    atomic_add_return
     

    For bit operand

    set_bit
    clear_bit
    change_bit
    test_bit
    test_and_set_bit
    test_and_clear_bit
    test_and_change_bit
     

    自旋锁

    Spin lock

    spinlock_t lock;
    spin_lock_init(lock);
    spin_lock(lock)
    spin_trylock(lock) return immediately even lock failure
    spin_unlock(lock)

    NOTE: 在自旋锁持有期间,内核抢占被禁止,主要针对SMP和单CPU可抢占内核,但是依然受到中断和底半部的影响。所以自旋锁往往结合中断使能函数一同使用,如下:
    spin_lock_irq
    spin_unlock_irq
    spin_lock_irqsave
    spin_unlock_irqrestore
    spin_lock_bh
    spin_unlock_bh

    NOTE: spin lock实际上是忙等,CPU不做任何事情,非常消耗CPU,所以只能用于很短时间的等待,往往用于等待硬件的场景
    spin lock可能导致系统死锁,比如递归使用同一个自旋锁,即拿到锁后,没有释放而再次拿锁
    spin lock锁定期间,不能调用可能引起进程调度的函数。如果进程获得自旋锁后再阻塞,如调用了copy_from_user(), copy_to_user(),则可能导致内核的崩溃
     

    读写自旋锁

    rwlock

    允许读的并发,禁止写的同时进行

    rwlock_init
    read_lock
    read_lock_ireqsave
    read_lock_irq

    read_unlock
    read_unlock_irqrestore
    read_unlock_irq

    write_lock
    write_lock_irqsave
    write_lock_irq
    write_trylock

    write_unlock
    write_unlock_irqrestore
    write_unlock_irq


    For example,

    rwlock_t lock;
    rwlock_init(&lock);

    read_lock(&lock);
    .....
    read_unlock(&lock);

    write_lock_irqsave(&lock, flags);
    ....
    write_unlock_irqrestore(&lock, flags);
     

    循序锁

    seqlock

    对读写锁的一种优化,读执行单元不会被写执行单元阻塞,写执行单元也不需要等待读执行单元完成读操作后才进行写操作,即读写可以同时操作,只不过如果读的过程中发生了写操作,需要重新读取数据;写执行单元之间是互斥的。

    因为顺序锁允许读写同时进行,大大提高了并发性,对于读写同时进行的概率比较小的情况下,性能非常好。

    NOTE:顺序锁有个限制,要求被保护的共享资源不含有指针,因为写执行单元可能使得指针失效,但读执行单元如果正要访问该指针,将导致oops

    For example,
    //write operation
    write_seqlock(&seqlock_a);
    ...
    write_sequnlock(&seqlock_a);

    //read operation
    read_seqbegin(...); //返回顺序锁的当前顺序号
    read_seqretry(...); //检查资源是否被复写,如是,则重读

    e.g.
    do {
       seqnum = read_seqbegin(&seqlock_a);
       /* execute read operations */
       ....
       
    } while(read_seqretry(&seqlock_a, seqnum));
     

    读-拷贝-更新

    RCU(Read-Copy Update)

    原理:写操作需要先拷贝一个副本,先对副本进行修改,然后再适当的时机拷贝(update)回原有数据。这个时机就是所有引用该数据的CPU都退出对共享数据的操作的时候。读执行单元没有任何同步开销,而写操作单元的同步开销则取决于使用的写执行单元间同步机制。

    RCU可以看做读写锁的高性能版本,既允许多个读单元并发,又允许多个读执行单元和多个写执行单元同时并发。但是对于写比较多的并发情况,写执行单元之间的同步开销也随之加大,必定需要用某种锁机制来同步并行的多个写操作

    RCU operation functions

    rcu_read_lock()
    rcu_read_unlock()
    实际上它们只是禁止和使能内核的抢占调度
    即,
    #define rcu_read_lock()   preempt_disable()
    #define rcu_read_unlock()   preempt_enable()

    synchronize_rcu()
    该函数由RCU写执行单元调用,它将阻塞执行单元,直到所有读执行单元完成。如果有多个CPU调用该函数,那么他们将在一个grace period之后全部被唤醒

    synchorize_kernel()
    内核代码使用该函数来等待所有CPU处于可抢占状态,目前功能等同于synchroize_rcu(),但现在已经不在使用,而是使用synchroize_sched();该函数用于等待所有的CPU处于可抢占状态,它能保证正在进行的中断处理函数处理完毕,但不能保证正在进行的软中断处理完毕

    call_rcu(struct rcu_head *head, void (*fucn)(struct rct_head *rcu));
    由写执行单元调用,不会使写执行单元阻塞,因而可以在中断上下文或者软中断使用;该函数把func挂接在RCU回调函数链上,然后立即返回。synchronize_rcu的实现使用了call_rcu函数

    RCU的链表版本,略
     

    信号量

    semaphore

    struct semaphore sem;
    void sema_init(struct semaphore *sem, int val);
    #define init_MUTEX(sem)   sema_init(sem, 1) //初始化互斥信号量
    #define init_MUTEX_LOCKED(sem) sema_init(sme, 0)
    DECLARE_MUTEX(name)
    DECLEAR_MUTEX_LOCKED(name)

    #获得信号量
    void down(struct semaphore *sem);
    int down_interruptible(struct semaphore *sem); //一旦阻塞进入睡眠可以被信号打断,信号会导致该函数返回,返回值为非0
    int down_trylock(struct semaphore *sem); //因为不会导致调用者睡眠,可以用在中断上下文

    #释放信号量
    void up(struct semaphore *sem);

    读写信号量

    读写信号量和信号量的关系如同读写自旋锁和自旋锁
    读写信号量可能引起进程阻塞,但它允许N个读执行单元同时访问共享资源,最多一个写执行单元。
    like
    down_read
    up_read
    down_write
    up_write

    完成量

    completion
    strcut completion
    init_completion
    void wait_for_completion(struct completion *c);
    void complete(struct completion *c);
    void complete_all(struct completion *c);

     

    互斥体

     

    struct mutex
    mutex_init
    mutex_lock
    mutex_unlock

     

    总结比较

     

    信号量是进程级的,用于多个进程间同步,使用信号量的开销是进程上下文切换的开销;因进程上下文切换的开销较大,所以只有当进程占用资源时间较长时,用信号量才是较好的选择;当临界资源访问时间很短,使用自旋锁较好。
    信号量可以阻塞即睡眠,自旋锁保护的资源不能够进入睡眠。阻塞意味着进程的切换,一旦进程切换出去,另一个进程企图获取本自旋锁,死锁就会发生
    信号量存在于进程上下文,如果共享(临界)资源在中断或者软中断下使用,则只能选择自旋锁或者down_trylock信号量。
     

    Block & Unblock

    Block: once not match certain condition, sleep to wait
    Unblock: if not ready, return to abort
     

    Wait queue

    等待队列 实现阻塞进程的唤醒
    NOTE:信号量的实现也依赖于等待队列

    wait_queue_head_t my_queue;
    init_waitqueue_head(&my_queue);
    OR DECLARE_WAITQUEUE(name, tsk);

    add_wait_queue(...)
    remove_wait_queue(...)

    wait_event(queue, condition)
    wait_event_interruptible(queue, condition)
    wait_event_timeout(queue, condition, timeout)
    wait_event_interruptible_timeout(queue, condition, timeout)

    wake_up(wait_queue_head_t *queue)
    wakt_up_interruptible(wait_queue_head_t *queue)

    sleep_on(wait_queue_head_t *q)
    interruptible_sleep_on(wait_queue_head_t *q)
    两个函数主要工作:将当前进程的状态设置成TASK_INTERRUPTIBLE,并定义一个等待队列,把它附属到等待队列头q,直到资源获得,q引导的等待队列被唤醒或者进程收到信号

    sleep_on <-> wake_up
    interruptible_sleep_on <-> wake_up_interruptible

    NOTE: 在很多驱动中,并不调用sleep_on或者interruptible_sleep_on,而是亲自进行进程的状态改变和切换,举例说明。

    xxx_write(...)
    {
    do {
        avail = device_writable(...);
        if (avail < 0)
            _ _set_current_state(TASK_INTERRUPTIBLE); //change state of process
        if (avail < 0) {
            if (file->f_flags & O_NONBLOCK)
                return - EAGAIN;
            schedule(); //switch to other process
            if (singal_pending(current)) //if wake up by signal
                return - ERESTARTSYS;
    }while(avail < 0);
    }

    NOTE: 以上代码不完整,并且存在错误,仅仅展示如何改变进程状态和schedule来简单展示如何让当前process进行sleep的,而没有用到sleep_on
     

    Polling轮询

    当读取非阻塞的设备时,需要应用不断检查设备是否就绪。除了忙等外,即不停的判断,使用select和poll系统调用来查询设备是否就绪

    select

    int select(int numfds, fd_set *readfds, fd_set *writefds, fd_set exceptfds, struct timeval *timeout)
    readfds,writefds,exceptfds分别是被select监视的读,写和异常处理的文件描述符集合

    FD_ZERO
    FD_SET
    FD_CLR
    FD_ISSET
     

    Poll

    unsigned int (*poll) (struct file *filp, struct poll_table *wait);
    poll_wait //把当前进程添加到wait参数指定的等待列表(poll_table)中
    驱动程序poll应该返回设备资源的可获取状态,即POLLIN,POLLOUT,POLLPRI等

    驱动中poll函数的典型模板为:

    statci unsigned int xxx_poll(struct file *filp, poll_state *wait)
    {
    ....
    poll_wait(filp, &dev->r_wait, wait);
    poll_wait(filp, &dev->w_wait, wait);

    if (...) /*readble*/
        mask |= POLLIN | POLLRDNORM;
    if (...) /*writable*/
        mask |= POLLOUT | POLLWRNORM;
    ...
    return mask;
    }

     

    非阻塞I/O编程

    设置非阻塞I/O的两种方式:open(O_NONBLOCK);fcntl(O_NONBLOCK)

    对于慢速I/O的读写设计思想:

    如果阻塞式操作,需要创建多线程来读写I/O,但是需要线程同步开销,例如线程A为主线程,负责控制和状态逻辑等操作,线程B专门来负责读写数据,A和B通过buffer和锁机制来同步;

    如果非阻塞方式,要么定周期轮询(polling)浪费CPU,要么采用I/O多路,显然后者是最好的。后者可以采用epoll,或者select方式;前者基本很少使用,因为效率低下并且实时性不如epoll/ select

     

    异步通知

     

    概述

    一旦设备就绪,则通过信号主动通知应用程序,类似于硬件上的中断概念

    可见除了阻塞I/O,非阻塞I/O+polling以外,还可以用异步通知来实现异步I/O

    进程间的信号有几十个,一个信号被捕获意思是当信号到达时有相应的代码处理它,如果没有被这个进程所捕获,内核将采用默认行为处理。注意:SIGKILL和SIGSTOP两个信号不能被捕获或者忽略
     

    信号的接收

    应用程序中,为了捕获信号,可以使用singal函数来设置相应信号的处理函数
    typedef void (*sighandler_t) (int);
    sighandler_t signal(int signum, sighandler_t handler);
    handler为处理函数,
    若为SIG_IGN表示忽略该信号;
    若为SIG_DFL表示采用系统默认方式处理;
    另一个改变进程接收特定信号后行为的函数为sigaction函数

    ctrl+c send SIGINT
    kill send SIGTERM

    通常用户空间处理一个设备释放的信号需要完成三个工作
    1. F_SETOWN IO控制命令设置设备文件的拥有者为本进程,这样设备驱动发出的信号才能被本进程接收到
    e.g. fcntl(fd, F_SETOWN, getpid())
    2. F_SETFL IO控制命令设置设备文件支持FASYNC,即异步通知模式
    e.g. fcntl(fd, F_GETFL)
    3. signal函数连接信号和信号处理函数
     

    信号的释放

    设备驱动中释放一个信号需要的三项工作
    1. 支持F_SETOWN命令,设置filp->f_owner为对应进程ID,有内核完成,驱动无需关注
    2. 支持F_SETFL命令的处理,当FASYNC标志改变时,驱动程序的fasync函数将得以执行。所以需要驱动实现fasync函数
    3. 在设备资源ready时,调用kill_fasync()函数激发相应的信号

    设备驱动中异步通知编程涉及的主要数据结构和函数
    struct fasync_struct
    fasync_helper //处理FASYNC标志变更的
    kill_fasync //释放信号用的函数
     

    Linux2.6异步I/O

    处理同步I/O以外,POSIX的异步I/O(AIO)的基本思想是允许进程发起很多I/O操作,而不用阻塞或者等待任何操作完成,稍后再接收I/O操作完成的通知时,在检索I/O操作的结果

    select提供的功能(异步阻塞I/O)与AIO类似,它对通知事件进行阻塞,而不是对I/O调用进行阻塞。JC:这里注意一下,I/O调用依然是非阻塞(即非阻塞I/O)的,但是select相当于通知事件进行拦截和封装,呈献给应用程序的,或者说是在应用程序视角看来,select是异步阻塞I/O操作。

    对于AIO来说,同时存在多个对设备的操作,用aiocb结构体来区分各个操作,以便标识I/O通知针对的操作项目,主要函数如下:
    aio_read
    aio_write
    aio_error
    aio_return //只有在aio_error调用确定请求已经完成后,再调用aio_return
    aio_suspend //挂起进程,直到异步请求完成为止
    aio_cancel
    lio_listio //在一个系统调用内,启动大量的I/O操作,发起多个传输
    以上都是用户空间如何利用AIO
     

    AIO机制中内核通知用户空间的方式

    1. 信号;
    2. 回调函数
    应用程序提供一个回调函数给内核,以便AIO的请求完成后内核调用这个函数。
    其中回调函数最终是请求了一个线程回调函数,具体请参考page 190

    总结下,块设备和网络设备本身是异步的,字符设备必须明确表明支持AIO,一般字符设备无需支持AIO。

     

    中断

    概述

    中断分为顶半部(top half)和底半部(bottom half)

    top half的工作主要是清除中断标记,挂载bottom half执行。

    top half往往设计为不可中断,bottom half则正好相反

     

    中断共享
    多个设备共用一根中短线的情况,在申请中断时使用IRQF_SHARED标志;每个中断例程都快速判断是否本设备中断。

    中断分类

    内部中断(软中断,溢出,除法错误等)和外部中断(外设)
    可屏蔽中断和不可屏蔽中断
    向量中断和非向量中断(前者由硬件提供ISR入口地址;后者有软件根据中断标志选择ISR入口)

     

    中断详解 - 摘录中断的知识

    本节内容 Quote from: http://blog.csdn.net/droidphone/article/details/7445825
    特别感谢:http://blog.csdn.net/droidphone

    中断控制器

    所有中断达到CPU之前,都会经过中断控制器汇集,符合要求的中断请求才能通知CPU

    中断控制器的工作:对irq的优先级进行控制,提供给CPU中断源(irq编号),使能(enable)或者屏蔽(mask)irq,清除中断请求(ack),这里注意,ack和enable和mask的区别。有些中断还需要CPU在处理完irq后对控制器发出eoi指令(end of interrupt),在smp系统中,控制各个irq与cpu之间的亲和性(affinity)

    中断控制器的软件抽象为irq_chip,其中的一堆函数操作正是中断控制器所能做的事情

     

    中断子系统框架

    #硬件封装层

    中断控制器被封装起来,形成了中断子系统的硬件封装层

    linux 中断向量表在arch/arm/kernel/entry-armv.S中定义

    vector_stub irq, IRQ_MODE, 4 //这一句把宏展开后实际上就是定义了vector_irq,根据进入中断前的cpu模式,分别跳转到__irq_usr或__irq_svc。

    vector_stub dabt, ABT_MODE, 8  // 这一句把宏展开后实际上就是定义了vector_dabt,根据进入中断前的cpu模式,分别跳转到__dabt_usr或__dabt_svc。

     

    #中断流控层

    电平触发中断(level type)
    边缘触发中断(edge type)
    简易的中断(simple type)

    fast eoi type(针对需要回应eoi(end of interrupt)的中断控制器)
    per cpu type(smp)
    以上不同类型的中断被抽象出来,成为了中断子系统的流控层



    流控层细节:
    进入C代码的第一个函数是asm_do_IRQ,在ARM体系中,这个函数只是简单地调用handle_IRQ,对应代码如下:
    arch/arm/kernel/entry-armv.S include “arch/arm/include/asm/entry-macro-multi.S”
    在entry-macro-multi.S中,bne     asm_do_IRQ。
    其中asm_do_IRQ最终调用了generic_handle_irq。

    以上是启动时的流程,不过对于任何一个中断的处理流程也是这样的。CPU一旦响应IRQ中断后,ARM会自动把CPSR中的I位置位,表明禁止新的IRQ请求,直到中断控制转到相应的流控层后才通过local_irq_enable()打开。这里说的流控层处理就是指generic_handle_irq被调用,最终调用到irq注册的流控层处理回调中,如下:
        static inline void generic_handle_irq_desc(unsigned int irq, struct irq_desc *desc)  
        {  
            desc->handle_irq(irq, desc);  
        }  

    通用中断子系统把几种常用的流控类型进行了抽象,并为它们实现了相应的标准函数,我们只要选择相应的函数,赋值给irq所对应的irq_desc结构的handle_irq字段中即可。

    这些标准的回调函数都是irq_flow_handler_t类型:
        typedef void (*irq_flow_handler_t)(unsigned int irq,  
                                struct irq_desc *desc); 
     

    #通用逻辑层

    中断通用逻辑层通过标准的封装接口(实际上就是struct irq_chip定义的接口)访问并控制中断控制器的行为。

    本层将作为硬件封装层和中断流控层以及驱动程序API层之间的桥梁,驱动程序和板级代码可以通过以下几个API设置irq的流控函数,实际上就是初始化irq_chip.

        irq_set_handler();
        irq_set_chip_and_handler();
        irq_set_chip_and_handler_name();

     

    目前的通用中断子系统实现了以下这些标准流控回调函数,这些函数都定义在:kernel/irq/chip.c中,
        handle_simple_irq  用于简易流控处理;
        handle_level_irq  用于电平触发中断的流控处理;
        handle_edge_irq  用于边沿触发中断的流控处理;
         handle_fasteoi_irq  用于需要响应eoi的中断控制器;
        handle_percpu_irq  用于只在单一cpu响应的中断;
        handle_nested_irq  用于处理使用线程的嵌套中断;

    #驱动程序API

    该部分向驱动程序提供了一系列的API,用于向系统申请/释放中断,打开/关闭中断,设置中断类型和中断唤醒系统的特性等操作。其中的一些API如下:

    申请和释放中断
    request_irq
    free_irq

    使能和屏蔽中断
    disable_irq
    disable_irq_nosync(同disable_irq区别在于,它部等待当前中断处理完成便立即返回)
    enable_irq

    屏蔽/恢复本CPU内所有的中断
    local_irq_save
    local_irq_disable
    local_irq_restore
    local_irq_enable

        irq_set_affinity(); // 亲和性
        request_threaded_irq();  //JC:先找到这个irq对应的irq_desc,然后把这个irq和对应的action注册到irq_desc中去
    其中,
        irq是要申请的IRQ编号,
        handler是中断处理服务函数,该函数工作在中断上下文中,如果不需要,可以传入NULL,但是不可以和thread_fn同时为NULL;
        thread_fn是中断线程的回调函数,工作在内核进程上下文中,如果不需要,可以传入NULL,但是不可以和handler同时为NULL;
        
        irqflags是该中断的一些标志,可以指定该中断的电气类型,是否共享等信息;
        devname指定该中断的名称;
        dev_id用于共享中断时的cookie data,通常用于区分共享中断具体由哪个设备发起;

    NOTE:beside request_threaded_irq, there is request _any_context_irq, request_percpu_irq.....

    IRQ的描述方式

    基于结构体struct irq_desc,其组织形式有数组方式和基数树(radix tree)动态分配方式。(后者宏开关:CONFIG_SPARSE_IRQ,一般都用动态分配的方式radix tree)

    系统中每一个注册的中断源,都会分配一个唯一的编号用于识别该中断,我们称之为IRQ编号。
    例如arch/mach-xxx/include/irqs.h

    IRQ系统的启动和工作流程

    IRQ系统的初始化
    (1) start_kernel -> setup_arch -> early_trap_init //完成中断向量拷贝和重定位工作(armv8-64和armv7可能都不太一样,要具体看代码

    位于arch/arm/kernel/traps.c中的early_trap_init()被调用,其中两个memcpy会把__vectors_start开始的代码拷贝到0xffff0000处,把__stubs_start开始的代码拷贝到0xFFFF0000+0x200处,这样,异常中断到来时,CPU就可以正确地跳转到相应中断向量入口并执行他们。

    (2) start_kernel -> early_irq_init -> alloc_desc //完成于硬件平台无关的代码,开辟内存给irq_desc
    (3) start_kernel -> init_IRQ //完成中断控制器的初始化,为每个irq_desc安装flow_handler and irq_chip,当然后面可以通过irq_set_chip_and_handler来安装

     

        系统启动阶段,取决于内核的配置,内核会通过数组或基数树分配好足够多的irq_desc结构;
        根据不同的体系结构,初始化中断相关的硬件,尤其是中断控制器;
        为每个必要irq的irq_desc结构填充默认的字段,例如irq编号,irq_chip指针,根据不同的中断类型配置流控handler;
        设备驱动程序在初始化阶段,利用request_threaded_irq() api申请中断服务,两个重要的参数是handler和thread_fn;
        当设备触发一个中断后,cpu会进入事先设定好的中断入口,它属于底层体系相关的代码,它通过中断控制器获得irq编号,在对irq_data结构中的某些字段进行处理后,会将控制权传递到中断流控层(通过irq_desc->handle_irq);
        中断流控处理代码在作出必要的流控处理后,通过irq_desc->action链表,取出驱动程序申请中断时注册的handler和thread_fn,根据它们的赋值情况,或者只是调用handler回调,或者启动一个线程执行thread_fn,又或者两者都执行;
        至此,中断最终由驱动程序进行了响应和处理。

     

    NOTE: 每个irq都会属于某一个irq_desc, 简单地说,逻辑关系是这样的,
    最顶层是一系列的irq_desc,每个irq_desc都有irq_data,对应的irq_chip(中断控制器的一堆操作指针函数),
    irq_desc里面还有个action list,其中每个action是一个特定irq的处理方式,上面包含handler和thread_fn

    中断响应过程的整个流程:
    H/W INT -> asm_do_IRQ() ->handle_IRQ
    handle_IRQ() <1>-> [soft-irq]irq_enter
    handle_IRQ() <2>-> [flow-handle流控层]generic_handle_irq ->action->handle -> irq_wake_thread() [desc->thread_fn]
    handle_IRQ() <3>-> [soft-irq]irq_exit -> invoke_softirq & __do_softirq
    at last, in irq-thread, run desc->thread_fn()
    NOTE: 特别注意:irq_desc的action结构体list,其中包含handler & thread_fn

    IRQ调试接口

    在/proc目录下面,有两个与中断子系统相关的文件和子目录,它们是:

        /proc/interrupts:文件
        /proc/irq:子目录

    读取interrupts会依次显示irq编号,每个cpu对该irq的处理次数,中断控制器的名字,irq的名字,以及驱动程序注册该irq时使用的名字

    /proc/irq目录下面会为每个注册的irq创建一个以irq编号为名字的子目录,每个子目录下分别有以下条目:

    • smp_affinity            irq和cpu之间的亲缘绑定关系;
    • smp_affinity_hint   只读条目,用于用户空间做irq平衡只用;
    • spurious                  可以获得该irq被处理和未被处理的次数的统计信息;
    • handler_name       驱动程序注册该irq时传入的处理程序的名字;

     

    中断线程化(Interrupt Threads)

    只要产生中断事件,内核将立即执行相应的中断处理程序,等到所有挂起的中断和软中断处理完毕后才能执行正常的任务,因此有可能造成实时任务得不到及时的处理。中断线程化之后,中断将作为内核线程运行而且被赋予不同的实时优先级,实时任务可以有比中断线程更高的优先级。这样,具有最高优先级的实时任务就能得到优先处理,即使在严重负载下仍有实时性保证。

     

    MISC

    以下是判断在中断上下文还是进程上下文的标志

    #definein_irq()                 (hardirq_count())  //硬中断上下文的判断
    #definein_softirq()          (softirq_count())  //软中断上下文的判断

    由上面的mark可以searching 代码从而看出真正在哪里设置的mark

     

    软中断也属于中断上下文,不能调用可以睡眠的函数,内核不允许,会panic

     

    如果普通软中断在执行完前,就是没有local_bh_enable之前,又来了硬中断,要来设相同的softirq的pending标志位,这是硬中断能否设置成功?如果设置成功,那么软中断不知道在它执行过程中,硬中断到底多少次设置pending bit位(插一句,看tasklet是个list,就可以避免这个问题)

     

    底半部机制

    tasklet,work queue,softirq
    实际上解决的问题,或者说要达到的目的是延时操作

    tasklet

    Tasklet概述

    在硬中断isr中调用tasklet_schedule(t),并且只会注册一个tasklet实例(类似单例),所以不必考虑SMP的并行问题。这样就避免了使用softirq所要考虑的互斥的问题。如果tasklet在执行tasklet->func()前,被多次调度,即调用tasklet_schedule,它只能推后执行

    Tasklet 实现细节:

    tasklet实现基于软中断HI_SOFTIRQ和TASKLET_SOFTIRQ,实际上他就是软中断的两种类型,只不过可以给kernel或者driver来使用的通用软中断,而其他软中断类型都是专用的,比如定时器,net rx和net tx

    Linux内核采用两个PER_CPU的数组tasklet_vec[]和tasklet_hi_vec[]维护系统种的所有tasklet(kernel/softirq.c),分别维护TASKLET_SOFTIRQ级别和HI_SOFTIRQ级别的tasklet。

     

    在start_kernel中,调用softirq_init来初始化softirq,其中核心代码就下面两行:

        open_softirq(TASKLET_SOFTIRQ, tasklet_action);
        open_softirq(HI_SOFTIRQ, tasklet_hi_action);

    解释下tasklet_action回调函数和大致工作:

     

    关闭本地中断的前提下,移出当前cpu的待处理tasklet链表到一个临时链表后,清除当前cpu的tasklet链表,之所以这样处理,是为了处理当前tasklet链表的时候,允许新的tasklet被调度进待处理链表中。

    遍历临时链表,用tasklet_trylock判断当前tasklet是否已经在其他cpu上运行,而且tasklet没有被禁止:

    • 如果没有运行,也没有禁止,则清除TASKLET_STATE_SCHED状态位,执行tasklet的回调函数。
    • 如果已经在运行,或者被禁止,则把该tasklet重新添加会当前cpu的待处理tasklet链表上,然后触发TASKLET_SOFTIRQ软中断,等待下一次软中断时再次执行。
    • tasklet有以下几个特征:
    • 同一个tasklet只能同时在一个cpu上执行,但不同的tasklet可以同时在不同的cpu上执行;
    • 一旦tasklet_schedule被调用,内核会保证tasklet一定会在某个cpu上执行一次;
    • 如果tasklet_schedule被调用时,tasklet不是出于正在执行状态,则它只会执行一次;
    • 如果tasklet_schedule被调用时,tasklet已经正在执行,则它会在稍后被调度再次被执行;
    • 两个tasklet之间如果有资源冲突,应该要用自旋锁进行同步保护;

    tasklet的API有:

    tasklet_init()
    tasklet_schedule()和tasklet_hi_schedule()
    tasklet_disable_nosync()、tasklet_disable()、task_enable()
    tasklet_action()和tasklet_hi_action() #具体执行函数
    tasklet_kill

    work queue

    struct work_struct my_wq;
    void my_wq_func(unsigned long);
    INIT_WORK(&my_wq, (void (*)(void *)) my_wq_func, NULL);
    schedule_work(&my_wq);
     

    softirq

    软件中断(softIRQ)是内核提供的一种延迟执行机制,它完全由软件触发,虽然说是延迟机制,实际上,在大多数情况下,它与普通进程相比,能得到更快的响应时间。

     

    open_softirq 注册软中断对应的处理函数
    raise_softirq函数触发一个软中断

    其中softirq和tasklet运行于softirq上下文,属于"原子"上下文的一种,而工作队列运行于进程上下文。因此软中断和tasklet处理函数不能睡眠,而工作队列允许睡眠。

    local_bh_disable() and local_bh_enable() 是内核中用于禁止和使能软中断和tasklet底半部机制的函数

    多个软中断可以同时在多个cpu运行,就算是同一种软中断,也有可能同时在多个cpu上运行。内核为每个cpu都管理着一个待决软中断变量(pending),它就是irq_cpustat_t:



    ### softirq macro-definition

    There're ten types of software interrupt defined in interrupt.h like HI_SOFTIRQ, TIMER_SOFTIRQ, NET_TX_SOFTIRQ, NET_RX_SOFTIRQ, ... SCHED_SOFT_IRQ ...

    ### softirq interrupt vector table
    h = softirq_vec;
    static struct softirq_action softirq_vec[NR_SOFTIRQS] = ...
    NR_SOFTIRQS is also defined in interrupt.h

    ### softirq exectution core function and when wakeup softirqd
    irq_exit() finally executes __do_softirq().
    In __do_softirq(), kernel executes each softirq one by one as priority, if threshold of max round (MAX_SOFTIRQ_RESTART) reach, but softirq is also set because of some hardware interrupt comming to set softirq again, then we do softirq in softirqd in process context rather than INT context. This can let process could be scheduled.

    ### softirq avoid race
    because __local_bh_disable_ip is executed during softirq; that's to say only one softirq routine could be running in same CPU core simultaneously

    ### instance of schedule() in ksoftirqd
    Because it will execute for long time sometimes, so it call schedule() by itself. 中文解释一下:大多数情况下,软中断都会在irq_exit阶段被执行,在irq_exit阶段没有处理完的软中断才有可能会在守护进程中执行。

     

    基于上面所说,软中断的执行既可以守护进程中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

     

    软中断有32位标记,每一位对应一种类型的软中断,硬中断ISR会设置其中某一个bit位而后触发软中断调度,软中断循环并依次判断每个bit位,若被置则执行相应的软中断程序,直到所有bit位都为空。在软中断执行期间,进程不会被调度执行,除非softirqd的触发门限到了,才会强制切换到进程上下文中

     

    对于驱动程序的开发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处理,这就需要使用tasklet机制。

     

    比较tasklet和softirq

    tasklet较之于softirq,tasklet不需要考虑SMP下的并行问题,而又比workqueues有着更好的性能

    tasklet和softirq区别(摘录:http://blog.csdn.net/wuxinyicomeon/article/details/5996695)

    软中断支持SMP,同一个softirq可以在不同的CPU上同时运行,softirq必须是可重入的。软中断是在编译期间静态分配的,它不像tasklet那样能被动态的注册或去除。软中断不能进入硬中断部分,且同一个CPU上软中断的执行是串行的,即不允许嵌套。因此,do_softirq()函数一开始就检查当前CPU是否已经正出在中断服务中,如果是则 do_softirq()函数立即返回。这是由do_softirq()函数中的 if (in_interrupt()) return; 保证的。

     

    引入tasklet,最主要的是考虑支持SMP,提高SMP多个cpu的利用率;不同的tasklet可以在不同的cpu上运行。tasklet可以理解为softirq的派生,所以它的调度时机和软中断一样。对于内核中需要延迟执行的多数任务都可以用tasklet来完成,由于同类tasklet本身已经进行了同步保护,所以使用tasklet比软中断要简单的多,而且效率也不错。tasklet把任务延迟到安全时间执行的一种方式,在中断期间运行,即使被调度多次,tasklet也只运行一次,不过tasklet可以在SMP系统上和其他不同的tasklet并行运行。在SMP系统上,tasklet还被确保在第一个调度它的CPU上运行,因为这样可以提供更好的高速缓存行为,从而提高性能。
    与一般的软中断不同,某一段tasklet代码在某个时刻只能在一个CPU上运行,但不同的tasklet代码在同一时刻可以在多个CPU上并发地执行。Kernel/softirq.c中用tasklet_trylock()宏试图对当前要执行的tasklet(由指针t所指向)进行加锁,如果加锁成功(当前没有任何其他CPU正在执行这个tasklet),则用原子读函数atomic_read()进一步判断count成员的值。如果count为0,说明这个tasklet是允许执行的。如果tasklet_trylock()宏加锁不成功,或者因为当前tasklet的count值非0而不允许执行时,我们必须将这个tasklet重新放回到当前CPU的tasklet队列中,以留待这个CPU下次服务软中断向量TASKLET_SOFTIRQ时再执行。为此进行这样几步操作:(1)先关 CPU中断,以保证下面操作的原子性。(2)把这个tasklet重新放回到当前CPU的tasklet队列的首部;(3)调用__cpu_raise_softirq()函数在当前CPU上再触发一次软中断请求TASKLET_SOFTIRQ;(4)开中断。

    软中断和tasklet都是运行在中断上下文中,它们与任一进程无关,没有支持的进程完成重新调度。所以软中断和tasklet不能睡眠、不能阻塞,它们的代码中不能含有导致睡眠的动作,如减少信号量、从用户空间拷贝数据或手工分配内存等。也正是由于它们运行在中断上下文中,所以它们在同一个CPU上的执行是串行的,这样就不利于实时多媒体任务的优先处理。


    那么,什么情况下使用工作队列,什么情况下使用tasklet。如果推后执行的任务需要睡眠,那么就选择工作队列。如果推后执行的任务不需要睡眠,那么就选择tasklet。另外,如果需要用一个可以重新调度的实体来执行你的下半部处理,也应该使用工作队列。它是唯一能在进程上下文运行的下半部实现的机制,也只有它才可以睡眠。这意味着在需要获得大量的内存时、在需要获取信号量时,在需要执行阻塞式的I/O操作时,它都会非常有用。如果不需要用一个内核线程来推后执行工作,那么就考虑使用tasklet。

    总结硬中断,软中断和信号

    硬中断是外部设备对CPU的中断,软中断是中断底半部的一种处理机制,而信号则是内核或者其他进程对某个进程的中断。注意:系统调用通过软中断(例如ARM为swi)陷入内核,此时软中断的概念是指由软件指令引发的中断,和我们这里说的softirq是两个不同的概念。


     

    Timer

    内核定时器

    硬件RTC提供两种中断:1. tick定周期的时钟信号;2. alarm
    软件定时器是基于硬件RTC来实现的,实现原理这里不做介绍
    定时器的API
    struct timer_list
    void init_timer(struct timer_list * timer);
    DECLARE_INITIALIZER(_function, _expires, _data)
    DEFINE_TIMER(_name, _function, _expires, _data)
    setup_timer
    add_timer
    del_timer
    mod_timer

    HZ为1s对应的tick数目,比如200
     

    delayed_work

    本质利用工作队列和定时器实现
    schedule_delayed_work
    cancel_delayed_work
     

    内核延时

    短延时

    ndelay,udelay,mdelay 空转CPU
    msleep和ssleep不能被打断
    msleep_interruptiable
     

    长延时

    比较当前的jiffies和目标jiffies,直到未来的jiffies达到目标的jiffies。
    time_before, time_after
     

    带休眠的延迟

    schedule_timeout() 使当前任务睡眠指定的jiffies之后重新被调度
    msleep本质上包含了schedule_timeout

     

    MMU

    基本概念

    MMU的作用:提供VA和PA的映射,内存访问权限保护和Cache缓存控制

    Linux内核使用了三级页表 PGD,PMD and PTE

    TLB Translation Lookaside Buffer  (cache 缓存va和pa的转换关系,俗称“快表”)

    TTW Translation Table walk (转换表漫游,即通过多级页表的访问来找到va和pa的对应关系,TTW成功后,结果写入TLB)

    注意:代码页和数据页往往是分开的,即数据TLB(DTLB)和指令TLB(ITLB)

    "Kernel Memory Layout on ARM Linux" in kernel/Documentation/arm/memory.txt

    Write-through VS Write-back/behind
    Write-through: Write is done synchronously both to the cache and to the backing store.
    Write-back (or Write-behind): Writing is done only to the cache. A modified cacheblock is written back to the store, just before it is replaced.

    常用数值:2^12 = 4K = 0x1000; 0xC000_0000 = 3 * 1024 * 1024 * 1024

    PAGE_OFFSET is different from PAGE_SHIFT. The former is alignment between kernel and user space, the latter is page size.

    PAGE_SHIFT is 12 (2^12 =  4K page size)

    PAGE_OFFSET is 0xC000_0000

     

    #definePUD_SHIFT       PGDIR_SHIFT

    #definePMD_SHIFT               21

    #definePGDIR_SHIFT             21

     

    pgd(Stage-2 page table) address:10-bit

    pte(page table)address: 10-bit

     

    274 enum zone_type {

     

    294         ZONE_DMA,

    309        ZONE_NORMAL,

    319        ZONE_HIGHMEM,

    321        ZONE_MOVABLE,

    322        __MAX_NR_ZONES

    323 };

     

    虚拟地址和物理地址的关系

    对于物理内存直接映射区的虚拟内存,使用virt_to_phys()来实现VA到PA的转化,对于ARM而言,virt_to_phys的定义如下:

    #define __virt_to_phys(x)    (((phys_addr_t)(x) - PAGE_OFFSET + PHYS_OFFSET))

     

     

     

    内存管理的各个区域

    内核将每个内存区域作为一个单独的内存对象管理,相应的操作也都一致。

    vm_area_struct是描述进程地址空间的基本管理单元,进程的虚拟地址空间由多个内存区域来描述,并分别由链表和红黑树组织起来,前者用于遍历所有节点,后者用于快速搜索

     

    匿名映射,没有映射到文件的都可以叫匿名映射
    匿名映射应该对应的就是匿名页面
    相对应的是文件页面,即映射到文件结点或者真实文件的页面(mmap)
     
    将物理地址与线性地址建立起对应关系。这种映射是由put_page(物理页地址,线性地址)来完成的,该函数通过参数所给的线性地址算出对应的页目录项和页表项,然后将物理页地址存入页表项,页表项地址存入目录项,以完成从线性地址到物理页地址的映射。
    Linux 2.6 引入了基于对象的反向映射机制。这种方法也是为物理页面设置一个用于反向映射的链表,但是链表上的节点并不是引用了该物理页面的所有页表项,而是相应的虚拟内存区域(vm_area_struct结构),虚拟内存区域通过内存描述符(mm_struct结构)找到页全局目录,从而找到相应的页表项。相对于前一种方法来说,用于表示虚拟内存区域的描述符比用于表示页面的描述符要少得多,所以遍历后边这种反向映射链表所消耗的时间也会少很多。

     

    每个用户进程的虚拟地址空间是独立的,页表也是独立的;

    内核的地址空间也是独立和固定的

     

    kernel地址空间由高到低依次划分为如下几个区域

    系统保留内存为最顶部:FIXADDR_TOP ~ 4GB

    专用页面映射区:FIXADDR_START ~ FIXADDRTOP (编译时预定义的)

    高端内存映射区:起始地址为PKMAP_BASE

    虚拟内存分配区(vmalloc):VMALLOC_START ~ VMALLOC_END

    物理内存直接映射区:最大长度为896MB,大于896MB的物理内存都称之为高端内存

     

    内存调试手段

    /proc/meminfo
    /proc/slabinfo

     

    内核空间的内存申请方式

    buddy system VS slab allocator

    buddy system and slab allocator都是内存分配的算法,前者用于以页为单位分配较大内存,后者用于小块内存的情况

     

    kmalloc, __get_free_pages and vmalloc.

    the two former is used to allocate memory in physical memory map area and continuous. VA and PA just offset.

    va vmalloc allocated is continuous but pa is maybe not continuous.

     

    kmalloc通过调用__get_free_pages实现,如果调用kmalloc使用GFP_KERNEL则表示若暂时无法分配内存,则睡眠,所以中断上下文(包括软中断,tasklet,内和定时器)和自旋锁期间不能调用kmalloc with GFP_KERNEL flag,应该用GFP_ATOMIC flag来申请kmalloc,当时候GFP_ATOMIC分配时,若不满足条件直接返回。

    __get_free_pages是linux最底层的用于获取空闲内存的方法,以page的2次幂的伙伴算法来管理和分配内存,所以用page为单位来分配

    get_zeroed_page()  // clear page

    __get_free_page()  //allocate one page

    __get_free_pages(unsigned int flags, unsigned int order) ; //allocate 2 ^ order pages

    以上函数最终调用了alloc_pages(), ......

    以上函数的flag参数常用的是GFP_KERNEL and GFP_ATOMIC,区别就是阻塞和非阻塞。

     

    vmalloc

    一般用于较大的顺序缓冲区分配内存,开销远大于__get_free_pages(), 为了完成vmalloc,新的页表被建立

    void *vmalloc(unsigned long size);

    void vfree(void *addr);

    vmalloc不能用于原子上下文中,其内部实现使用了标志为GFP_KERNEL的kmalloc()。

     

    slab与内存池

    (实际上kmalloc是使用slab机制来实现的)

    slab多用于分配较小的内存并且重复内存的情况,比如task_struct等。

    创建slab缓存:kmem_cache_create

    分配slab缓存:kmem_cache_alloc

    释放slab缓存:kmem_cache_free

    回收slab缓存:kmem_cache_destroy

     

    /proc/slabinfo

     

    slab实现依然依赖于__get_free_pages(), 只不过申请页面后,再分隔这些页为更小的单元并进行管理,从而节省了内存,提高slab缓冲对象的访问效率。

     

    除了slab以外,kernel还包含对内存池的支持。内存池技术是非常经典的用于分配大量小对象的后备缓存技术。

     

    kernel内存池

    创建内存池:mempool_create

    分配和回收对象:mempool_alloc; mempool_free

    回收内存池:mempool_destroy

     

    SPRD ThreadX的内存池设计思想,这种思想也是普遍采用的

    byte pool and block pool.

    byte pool以字节为单位分配,可以连续分配任意大小的内存

    block pool以多种预先设定的固定大小为单位分配,避免了碎片,但是也降低了灵活性

     

    设备IO端口和I/O内存

     

    通常来说,控制设备的寄存器如果位于I/O空间时,称为I/O端口;位于内存空间时,对应的内存空间为I/O内存

    (ARM而言,不存在I/O端口的概念)

    对于I/O端口是通过特殊的读取指令来访问的;

    对于I/O内存,首先使用ioremap函数将设备所处的物理地址映射到虚拟地址空间。ioremap也需要建立新的页表。释放用ioumap。

    I/O内存除了通过指针直接读写以外,还可以用下面的函数完成

    ioread8,ioread16,ioread32

    readb,readw,readl

    等等

     

    申请和释放设备的I/O端口和I/O内存

    I/O端口申请:requeset_region, release_region

    I/O内存申请:request_mem_region, release_mem_region

    I/O内存访问流程:request_mem_region -> ioremap -> ioread8 ..... -> ioumap -> release_mem_region

     

    设备地址映射到用户空间 mmap、munmap

    mmap可以使用户空间直接访问设备的物理地址,它将用户空间的一段内存和设备内存进行关联,当用户访问用户空间的这段地址范围时,实际上会转化为对设备的访问。

    这种映射以page为单位,系统调用mmap会最终调用到驱动中的mmap函数。

    mmap大致的流程如下:

    在进程的虚拟空间中找到一块VMA,将其进行映射;

    如果设备驱动程序或者文件系统的file_operations定义了mmap操作,则调用它;

    将这个VMA插入进程的VMA链表中

    驱动的mmap函数会建立新的页表,并填充VMA结构体vm_operations_struct指针。VMA即vm_area_struct,用于描述一个虚拟内存区域。

     

    创建页表有两个方式:nopage() and remap_pfn_range(),后者一般用于设备内存映射,前者还可以用于RAM映射,nopage掉用于缺页异常时。

     

    DMA

    DMA和cache一致性问题。cache是CPU内部的,针对内存的缓存。

    DMA是内存和外设之间的高速传输通道。当DMA涉及的内存有被cached的部分,那么CPU不知道这部分cache已经和内存上的数据不一致了(脏了)。

    往往看似非常离奇的想象,很可能是cache导致的

    解决DMA导致的cache一致性问题的方法是禁止DMA目标地址范围内的内存的cache功能。

    DMA的操作接口(略)

     

    #表初始化

    start_kernel -> setup_arch ->early_paging_init; sanity_check_meminfo; paging_init;

     

    sanity_check_meminfo

    检测判断是否需要创建highmem区,并且重建描述内存bank的数组. VMALLOC_MIN定义了vmalloc区的起始位置(通过VMALLOC_END和vmalloc_reserve计算得出),VMALLOC_END定义了vmalloc区的结束位置,vmalloc_reserve是系统预留给vmalloc区的大小。

     

    paging_init

    build_mem_type_table这个函数根据CPU类型,设置mem_types全局数组,mem_types数组保存了页目录和页表的属性,将来创建页目录和页表时,会用到mem_types。

     

    prepare_page_table这个函数会请空页目录,有两块地址空间区域是不需要清除的,一个是kernelimage,另外一个是vmalloc+ 永久内核映射以及固定映射线性地址。 

    其中memblock.memory.regions[0].base,memblock.memory.regions[0].size记录lowmem的物理地址地址和大小

     

    map_lowmem建立低端内存的所有页目录和页表:遍历memorybank,映射那些没有highmem标记的内存bank

    arch/arm/kernel/vmlinux.lds.S_stext

     

    devicemaps_init这个函数创建device的映射,

    1. 把machine vectors映射到0xffff0000处

    2. 调用平台特定的map_io,对于mx51,这个函数主要是映射mx51功能寄存器区, AIPS1 AIPS2 和SPBA0,这三个寄存器区大小为1MB,映射后的虚拟地址分别为0xF7E00000,0xF7D00000,0xFB100000

     

    kmap_init永久内核映射和固定映射的线性区域;创建pkmap的pgd和pte。并且让pkmap_page_table指向这个PTE page的linux p/t。一般来说kmap都是使用一个page的pte来映射高端内存到内核地址空间,对于arm来说,每个page可以存放512个pte_t,所以pkmap的地址空间为2M。

     

    bootmem_init :

     

    1. 调用check_initrd获取initrd所在的memory bank对应的node

     

    2. 对每一个节点:

     

    获取该node的 min(最小pfn), node_low(最大low memory pfn), node_hight(最大high memory pfn)

    调用bootmem_init_node初始化node,bootmem_init_node会初始化bootmem bitmap

     

    如果是node 0,那么调用reserve_node_zero为node 0 reserve的内存:内核text和data区,初始化页表区(16KB),以及swapper_pg_dir之前的

    一块内存(在我的机器上是4个page)

    如果initrd存放在当前的node上,那么调用bootmem_reserve_initrd保留initrd占用的内存。initrd_start是initrd在起始虚拟内存地址,

    initrd_end是initrd结束的虚拟内存地址。

    3. 对每一个node 调用bootmem_free_node

     

    设置这个node内各个zone的大小

    调用free_area_init_node:计算node的总pages数目,为这个node分配mem map,注意node内所有zone的memmap都分配在一起

    调用free_area_init_core:对于node内的每一个zone,进行初始化。注意这个函数present_pages是total size减去了该分区对应的memmap占用

    的pages,但是实际上memmap是放在node的开始位置,这里似乎不应该减去这个值

     

    4.high_memory 是一个很奇怪的变量,high_memory应该是物理内存的概念,但是high_memory变量保存的确实一个内核地址。

     

    Linux设备驱动

    设备驱动模型:总线,设备和驱动

    linux虚拟总线:platform总线,platform device and platform driver,比如I2C,RTC,LCD等控制器归纳为platform device

    其中platform bus的数据结构为bus_type, 其对应的实例为plaftform_bus_type

     

    第三篇 Linux设备驱动实例

     

    第四篇 Linux设备驱动调试,移植

     

     

     

     

     

    展开全文
  • 在终端用户看来,USB设备为主机提供了多种多样的附加功能,如文件传输,声音播放等,但对USB主机来说,它与所有USB设备的接口都是一致的。一个USB设备由3个功能模块组成:USB总线接口、USB逻辑设备和功能单元: ...

            在终端用户看来,USB设备为主机提供了多种多样的附加功能,如文件传输,声音播放等,但对USB主机来说,它与所有USB设备的接口都是一致的。一个USB设备由3个功能模块组成:USB总线接口USB逻辑设备功能单元:

    a -- 这里的USB总线接口指的是USB设备中的串行接口引擎(SIE);

    b -- USB逻辑设备被USB系统软件看作是一个端点的集合

    c -- 功能单元被客户软件看作是一个接口的集合。SIE、端点和接口都是USB设备的组成单元;


            为了更好地描述USB设备的特征,USB提出了设备架构的概念。从这个角度来看,可以认为USB设备是由一些配置接口端点组成,即一个USB设备可以含有一个或多个配置,在每个配置中可含有一个或多个接口,在每个接口中可含有若干个端点。其中,配置和接口是对USB设备功能的抽象,实际的数据传输由端点来完成。在使用USB设备前,必须指明其采用的配置和接口。这个步骤一般是在设备接入主机时设备进行枚举时完成的

    这些单元之间的关系如下:



    设备通常有一个或多个配置;

    配置通常有一个或多个接口;

    接口通常有一个或多个设置;

    接口有零或多个端点。


           这样的概念太抽象了,可以这样看:有一个设备,如支持视频和音频的一个播放器。那么,对于上面提到的4个描述符,对它们设置的时候,它们分别对于哪一个描述符呢?  

          从我现在的理解来看,这样一个设备对应一个设备描述符,支持视频的功能对应一个接口描述符,支持音频功能的对应一个接口描述符。为了支持视频,在下层有多个端口同时工作为提供视频数据传输的支持,所以有多个端点描述符



          USB设备使用各种描述符来说明其设备架构,包括设备描述符、配置描述符、接口描述符、端点描述符和字符串描述符,他们通常被保存在USB设备的固件程序中

    1、设备描述符

          设备代表一个USB设备,它由一个或多个配置组成。设备描述符用于说明设备的总体信息,并指明其所含的配置的个数。一个USB设备只能有一个设备描述符

    struct usb_device_descriptor
    {
    	_ _u8 bLength; //描述符长度
    	_ _u8 bDescriptorType; //描述符类型编号
    
    	_ _le16 bcdUSB; //USB版本号
    	_ _u8 bDeviceClass; //USB分配的设备类code
    	_ _u8 bDeviceSubClass;// USB分配的子类code
    	_ _u8 bDeviceProtocol; //USB分配的协议code
    	_ _u8 bMaxPacketSize0; //endpoint0最大包大小
    	_ _le16 idVendor; //厂商编号
    	_ _le16 idProduct; //产品编号
    	_ _le16 bcdDevice; //设备出厂编号
    	_ _u8 iManufacturer; //描述厂商字符串的索引
    	_ _u8 iProduct; //描述产品字符串的索引
    	_ _u8 iSerialNumber; //描述设备序列号字符串的索引
    	_ _u8 bNumConfigurations; //可能的配置数量
    } _ _attribute_ _ ((packed));

    2、配置描述符

           一个USB设备可以包含一个或多个配置,如USB设备的低功耗模式和高功耗模式可分别对应一个配置。在使用USB设备前,必须为其选择一个合适的配置。配置描述符用于说明USB设备中各个配置的特性,如配置所含接口的个数等。USB设备的每一个配置都必须有一个配置描述符。

    struct usb_config_descriptor
    {
    	_ _u8 bLength; //描述符长度
    	_ _u8 bDescriptorType; //描述符类型编号
    	
    	_ _le16 wTotalLength; //配置所返回的所有数据的大小
    	_ _u8 bNumInterfaces; // 配置所支持的接口数
    	_ _u8 bConfigurationValue; //Set_Configuration命令需要的参数值
    	_ _u8 iConfiguration; //描述该配置的字符串的索引值
    	_ _u8 bmAttributes; //供电模式的选择
    	_ _u8 bMaxPower; //设备从总线提取的最大电流
    } _ _attribute_ _ ((packed));

    3、接口描述符

          一个配置可以包含一个或多个接口,例如对一个光驱来说,当用于文件传输时,使用其大容量存储接口;而当用于播放CD时,使用其音频接口。接口是端点的集合,可以包含一个或多个可替换设置,用户能够在USB处于配置状态时改变当前接口所含的个数和特性。接口描述符用于说明设备中各个接口的特性,如接口所属的设备类及其子类等。USB设备的每个接口都必须有一个接口描述符

    struct usb_interface_descriptor
    {
    	_ _u8 bLength;           //描述符长度
    	_ _u8 bDescriptorType; //描述符类型
    	
    	_ _u8 bInterfaceNumber;   // 接口的编号
    	_ _u8 bAlternateSetting; //备用的接口描述符编号
    	_ _u8 bNumEndpoints;      //该接口使用的端点数,不包括端点0
    	_ _u8 bInterfaceClass;    //接口类型
    	_ _u8 bInterfaceSubClass; //接口子类型
    	_ _u8 bInterfaceProtocol; //接口所遵循的协议
    	_ _u8 iInterface; //描述该接口的字符串索引值
    } _ _attribute_ _ ((packed));

    4、端点描述符

          端点是USB设备中的实际物理单元,USB数据传输就是在主机和USB设备各个端点之间进行的。端点一般由USB接口芯片提供,例如Freescale公司的MC68HC908JB8和MC9S12UF32。USB设备中的每一个端点都有唯一的端点号,每个端点所支持的数据传输方向一般而言也是确定的:或是输入(IN),或是输出(OUT)。也有些芯片提供的端点的数据方向是可以配置的,例如MC68HC908JB8包含有两个用于数据收发的端点:端点1和端点2。其中端点1只能用于数据发送,即支持输入(IN)操作;端点2既能用于数据发送,也可用于数据接收,即支持输入(IN)和输出(OUT)操作。而MC9S12UF32具有6个端点。

         利用设备地址、端点号和传输方向就可以指定一个端点,并与它进行通信。端点的传输特性还决定了其与主机通信是所采用的传输类型,例如控制端点只能使用控制传输。根据端点的不同用途,可将端点分为两类:0号端点和非0号端点。

          0号端点比较特殊,它有数据输入IN和数据输出OUT两个物理单元,且只能支持控制传输。所有的USB设备都必须含有一个0号端点,用作默认控制管道。USB系统软件就是使用该管道与USB逻辑设备进行配置通信的。0号端点在USB设备上的以后就可以使用,而非0号端点必须要在配置以后才可以使用。

         根据具体应用的需要,USB设备还可以含有多个除0号端点以外的其他端点。对于低速设备,其附加的端点数最多为2个;对于全速/高速设备,其附加的端点数最多为15个。

    struct usb_endpoint_descriptor
    {
    	_ _u8 bLength; //描述符长度
    	_ _u8 bDescriptorType; //描述符类型
    	_ _u8 bEndpointAddress; //端点地址:0~3位是端点号,第7位是方向(0-OUT,1-IN)
    	_ _u8 bmAttributes; //端点属性:bit[0:1] 的值为00表示控制,为01表示同步,为02表示批量,为03表示中断
    	_ _le16 wMaxPacketSize; //// 本端点接收或发送的最大信息包的大小
    	_ _u8 bInterval;//轮询数据传送端点的时间间隔
    	                       //对于批量传送的端点以及控制传送的端点,此域忽略
    	                    //对于同步传送的端点,此域必须为1
    	_ _u8 bRefresh;
    	_ _u8 bSynchAddress;
    } _ _attribute_ _ ((packed));

    5、字符串描述符

          在USB设备中通常还含有字符串描述符,以说明一些专用信息,如制造商的名称、设备的序列号等。它的内容以UNICODE的形式给出,且可以被客户软件所读取。对USB设备来说,字符串描述符是可选的。

    struct usb_string_descriptor
    {
    	_ _u8 bLength; //描述符长度
    	_ _u8 bDescriptorType; //描述符类型
    	
    	_ _le16 wData[1];
    } _ _attribute_ _ ((packed));


    6、管道

          在USB系统结构中,可以认为数据传输时在USB主机软件与USB设备的各个端点之间直接进行的,它们之间的连接称为管道。管道是在USB设备的配置过程中建立的。管道是对USB主机与USB设备间通信流的抽象,表示USB主机的数据缓冲区与USB设备的端点之间存在着逻辑数据传输,而实际的数据传输是由USB总线接口层来完成的。

         管道与USB设备中的端点一一对应。一个USB设备含有多少个端点,其与USB主机进行通信时就可以使用多少条管道,且端点的类型决定了管道中数据的传输类型,例如中断端点对应中断管道,且该管道只能进行中断传输。不论存在着多少条管道,在各个管道中进行的数据传输都是相互独立的。


    7、USB端点分类

          USB 通讯的最基本形式是通过端点。一个USB端点只能向一个方向传输数据(从主机到设备(称为输出端点)或者从设备到主机(称为输入端点))。端点可被看作一个单向的管道。

         USB 端点有 4 种不同类型, 分别具有不同的数据传送方式:

    1) 控制CONTROL 

        控制端点被用来控制对USB设备的不同部分访问. 通常用作配置设备、获取设备信息、发送命令到设备或获取设备状态报告。这些端点通常较小。每个 USB 设备都有一个控制端点称为"端点 0", 被 USB 核心用来在插入时配置设备。USB协议保证总有足够的带宽留给控制端点传送数据到设备.

    2) 中断INTERRUPT 

         每当 USB 主机向设备请求数据时,中断端点以固定的速率传送小量的数据。此为USB 键盘和鼠标的主要的数据传送方法。它还用以传送数据到USB设备来控制设备。通常不用来传送大量数据。USB协议保证总有足够的带宽留给中断端点传送数据到设备.

    3) 批量BULK

        批量端点用以传送大量数据。这些端点通常比中断端点大得多. 它们普遍用于不能有任何数据丢失的情况。USB 协议不保证传输在特定时间范围内完成。如果总线上没有足够的空间来发送整个BULK包,它被分为多个包进行传输。这些端点普遍用于打印机、USB Mass Storage和USB网络设备上。

    4) 等时ISOCHRONOUS 

        等时端点也批量传送大量数据, 但是这个数据不被保证能送达。这些端点用在可以处理数据丢失的设备中,并且更多依赖于保持持续的数据流。如音频和视频设备等等。

        控制和批量端点用于异步数据传送,而中断和等时端点是周期性的。这意味着这些端点被设置来在固定的时间连续传送数据,USB 核心为它们保留了相应的带宽。

    struct usb_host_endpoint{
        struct usb_endpoint_descriptor desc;//端点描述符
        struct list_head urb_list;//此端点的URB对列,由USB核心维护
        void *hcpriv;
        struct ep_device *ep_dev; /* For sysfs info */
        unsigned char*extra;/* Extra descriptors */
        int extralen;
        int enabled;
    };
    

         当调用USB设备驱动调用usb_submit_urb提交urb请求时,将调用int usb_hcd_link_urb_to_ep(struct usb_hcd *hcd, struct urb *urb)把此urb增加到urb_list的尾巴上。(hcd: Host Controller Driver,对应数据结构struct usb_hcd )



     



    展开全文
  • 驱动概述  说到 android 驱动是离不开 Linux 驱动的。Android 内核采用的是 Linux2.6 内核 (最近Linux 3.3 已经包含了一些 Android 代码)。...android 驱动 主要分两种类型:Android 专用驱动 和 Android 使用
  • 一.Linux系统设备驱动程序概述   1.1 Linux设备驱动程序分类   1.2 编写驱动程序的一些基本概念  二.Linux系统网络设备驱动程序   2.1 网络驱动程序的结构   2.2 网络驱动程序的基本方法   2.3 ...
  • 5.linux设备驱动模型

    2019-05-05 16:49:56
    1.linux设备驱动模型简介 1.1、什么是设备驱动模型 (1)类class、总线bus、设备device、驱动driver (2)kobject和对象生命周期 (3)sysfs (4)udev 1.2、为什么需要设备驱动模型 (1)早期内核(2.4之前)没有统一的...
  • #include 是在linux-2.6.29/include/linux下面寻找源文件。 #include 是在linux-2.6.29/arch/arm/include/asm下面寻找源文件。 #include 是在linux-2.6.29/arch/arm/mach-s3c2410/include/mach下面寻找源文件。 #...
  • 我做嵌入式驱动开发已经一年多了,感觉没学到多少东西。 从网络驱动到CAN驱动、从dataflash驱动到NAND/NOR FLASH驱动、从CF/IDE驱动到各种嵌入式文件系统、从LCD/VGA驱动到音频驱动、从IAR到ADS再到GCC、Ucos到...
  • IIC(Inter-Integrated Circuit)总线是一种由PHILIPS公司开发的两线式串行总线,用于连接微控制器及其外围设备。IIC总线产生于在80年代,最初为音频和视频设备开发,如今主要在服务器管理中使用,其中包括单个组件...
  • 下面是一个最简单的字符驱动程序框架,下面我将一一进行分析里面的内容。 #include &lt;linux/fs.h&gt; #include &lt;linux/init.h&gt; /* 定义一个open函数 */ static int first_drv_open...
  • linux驱动编写(虚拟字符设备编写)  昨天我们说了一些简单模块编写方法,但是终归没有涉及到设备的编写内容,今天我们就可以了解一下相关方面的内容,并且用一个实例来说明在linux上面设备是如何编写的。 ...
  • linux下写驱动是站在巨人的肩膀上做开发,不用完全从头做起,甚至你不需要懂SPI时序,照样能写出可用的驱动,原因是:控制器驱动一般芯片商早已写好(linux4.9中针对imx系列cpu是 drivers/spi/spi-imx.c),怎么发、...
  • HOST 部分是针对不同主机的驱动程序,这一部是驱动程序工程师需要根据自己的特点平台来完成的。 CORE 部分: 这是整个MMC 的核心存,这部分完成了不同协议和规范的实现,并为HOST 层的驱动提供了接口函数。 CARD 部分...
  • Linux设备驱动开发详解(第2版)》前言
  • 本系列文章将分析Linux Framebuffer驱动的作用(需求)、框架、接口实现和使用。按笔者一直倡导的Linux学习理念—从软件需求的角度去理解Linux,对于Linux各个子系统,我们首先要理解其软件需求,从中自然会清楚其...
  • 嵌入式设计及Linux驱动开发指南——基于ARM9处理器  读书笔记  第一章嵌入式系统基础  1、 嵌入式系统定义:  “嵌入式系统是用来控制或者监视机器、装置、工厂等大规模系统的设备。”  ——...
  • Linux驱动开发之 二 (那些必须要了解的硬件知识 之 处理器篇)本文中部分内容源自网络,如有侵权请联系博主,谢谢老谢一直以“软硬通杀”的标准来要求自己。搞软件的工程师,有必要了解底层硬件的基本原理;搞驱动的...
  • Linux设备驱动开发详解(第2版)(前一版狂销3万册,畅销书最新升级) [新品]
1 2 3 4 5 ... 20
收藏数 19,399
精华内容 7,759