Sysfs文件系统(一):从serio virtual bus开始说起

serio是一种虚拟总线,先上一副图

大概就是这样的,驱动程序注册一个serio port; 当接受硬件传上来的数据的时候通过serio port将数据传递给在总线另外一端的serio driver。当然由于这种结构的出现也就使得一个serio driver可以对应多个serio port(serio driver只是一个数据处理的程序罢了)。


bus的种类很多了,除了serio bus, 还有很多。

具体可以看下/sys/bus下面的文件夹,每一个文件夹的名字就是一种总线。随便打开一个文件夹里面都有两个子文件夹:devices和drivers。(在serio总线中serio port就是device,而serio driver就是driver)它们的架构都大同小异。

 具体示例程序可以看Linux源代码中的/driver/input中的程序,都是以这种模式编写的。

同样的,这种模式还有一个至关重要的好处:就是对于硬件厂商来说,他们只需要提供某种类型总线的driver就好了,而这样的程序是非常好写的,只需要经过简单的培训,一个对Linux驱动程序没有一点了解的程序员都能够写出。

来看下device和driver是如何匹配的,我们用serio总线的代码来做一个示例看下。

static int serio_match_port(const struct serio_device_id *ids, struct serio *serio)
{
    while (ids->type || ids->proto) {
        if ((ids->type == SERIO_ANY || ids->type == serio->id.type) &&
            (ids->proto == SERIO_ANY || ids->proto == serio->id.proto) &&
            (ids->extra == SERIO_ANY || ids->extra == serio->id.extra) &&
            (ids->id == SERIO_ANY || ids->id == serio->id.id))
                return 1;
        ids++;
    }
    return 0;
}

其实就是在serio port和serio driver中各有一个id table, 如果两边match,则就匹配成功。

最后给推荐一篇文章:
http://blog.chinaunix.net/u1/51562/showart_1083392.html
这个是对serio virtual bus源代码的分析,相信有了这样的整体概念,这个代码分析就比较轻松了。其实Linux内核代码多数都不会很难,只需要知道整体的架构和设计人员的用意之后,代码就像看小说一样。

Linux内核中的两个宏: likely & unlikely

可以在驱动中经常看到这样的代码:
if(likely( x > 1)){
      /* … */
}
else {
      /* … */
}
一开始不知道是做什么用的,后来问了海哥,他说是一个编译优化的宏,这东西表示likely()中的那个表达式成立的概率比较高,就会让cache中优先存放if之后的语句。
今天去查了下这两个宏的定义:

#define likely(x)       __builtin_expect((x),1)
#define unlikely(x)     __builtin_expect((x),0)
__builtin_expect((x),1) 表示的是我们对x表达式成立的期望值是1.
这样编译器编译出汇编代码的时候就会把if下面的语句紧接着之前的代码.
而unlikely(x)则会把 else之后的代码紧接着之前的代码。这样cpu就会优先把那部分代码放到cache中去。

Linux内核编程中的计时工具

内核中经常需要把握时间过了多长。这篇文章就是大概介绍一下内核中的计时方法。
jiffies, HZ
Hz物理上的意义是频率的单位,在Linux内核中,HZ是系统计时器频率值的常量,即一秒钟系统计时器增加几次
X86体系架构下,在2.4内核中HZ被默认的设置为100,2.6中是1000,但是2.6.13中却是250. ARM体系架构下,2.6内核中HZ被设置为100.
jiffies是自开机以来系统计时器增加的总次数。所以jiffies的值是与HZ的值直接相关的。
HZ是多少,则jiffies的值每一秒就增加多少。比如说当HZ是100的时候,jiffies的值每一秒就增加100。
有一点注意:jiffies是一个32位无符号整型数。当HZ值是1000的时候,一秒钟增加1000则jiffies会在49.7天这样的时候就溢出了。而一台服务器开启的时间往往会远超于这个时间。所以内核提供了这样一个变量jiffies_64, 这是一个64位无符号整型数。这样就会处理很长很长的时间了。
注意:在32位机上,jiffies_64其实是由两个32位无符号整型数组合起来的,它的低32位部分就是jiffies,这样就造成了读取jiffies_64不是一个原子操作。所以在读取jiffies_64时建议不要直接使用jiffies_64, 而是使用get_jiffies_64()这样一个内核辅助函数。
现在来看在代码中如何使用这个计时工具。
下面的代码判断某一些工作是否在1秒钟之内完成:
unsigned long timeout = jiffies + HZ;
/* ..do something.. */
if(time_after(jiffies, timeout)) /*超过1秒钟*/
下面的代码忙等1秒钟时间:
unsigned long timeout = jiffies + HZ;
while(time_before(jiffies, timeout)) continue;
这样的忙等法当然不好,看下面的代码会让出CPU1秒不执行自己的代码之后再回来继续自己的执行:
unsigned long timeout = jiffies + HZ;
schedule_timeout(timeout);

中断处理之三:阻塞型I/O

应用程序向硬件请求数据的时候,硬件那不一定有数据上来。怎么办呢?

简单的想法就是忙等。这样效率低,所以这里就介绍一下Linux中的阻塞型输入输出。

wait_queue_heat_t结构体

这个结构体是用来管理睡眠唤醒的条件的。不同的等待事件我们要为其创建不同的等待队列。

定义等待队列方法如下:

wait_queue_head_t sample_wait_queue;
init_waitqueue_head(sample_wait_queue);

或者

DECLARE_WAIT_QUEUE_HEAD(sample_wait_queue);

如何使用?

使用方法比较简单,在先定义了等待队列之后

当进程需要睡眠的时候(比如等待数据的到来)就调用

interruptible_sleep_on(&sample_wait_queue);

来使得当前进程睡眠;

当数据来的时候就调用

wake_up_interruptible(&sample_wait_queue);

唤醒进程。

这里有一个问题:要在哪里唤醒进程呢

答案是:在中断处理函数中,在有数据来的时候,会发生中断,这时候中断处理函数就会被调用(当然你要申请过IRQ并且注册了该中断处理函数)

所以就会经常看到这样结构的代码:

/* 中断处理函数 */
void XXX_interrupt(int irq, void *dev_id, struct pt_regs *regs)
{
    /* … */
    wake_up_interrupt(&waitqueue);

    /* … */
}

/* 阻塞式读入 */
ssize_t XXX_read(struct file *filp, char *buf, size_t count,
                  loff_t *f_ops)
{
    /* … */
    interruptible_sleep_on(&waitqueue);
   
    /* … */
}

中断处理之二: Top half & Bottom half

中断处理函数(interrupt handler)有一个要求就是要短小精悍,运行时间越短越好。因为在中断处理函数之前,会先关中断,那么在中断处理函数执行的这段时间内,所有发生的中断将都不能被处理。但是很多时候我们在中断处理中必须要做很多事情,这时候怎么办呢?

内核里面提供了bottom half这样的机制使得我们可以达到这样的目的。

Top half & Bottom half
中断处理过程被分成了两个部分,top half会将所需要作的工作全部交给bottom half去做然后自己立刻返回。在内核中提供了下面三种机制可以让我们实现bottom half: softirqs, tasklets 和 work queues.


Softirq是基础的bottom half机制, tasklet是建立在softirq之上的。这二者的主要区别在于Softirq是可重入的,而tasklet是不可重入的。

以下是示例代码

Softirq示例代码:

void __init softirq_sample_init()
{
    /* … */
   
    /*调用open_softirq打开自定义的SAMPLE_SOFT_IRQ软中断*/

    open_softirq(SAMPLE_SOFT_IRQ, softirq_sample_bottom, NULL);

    /*SAMPLE_SOFT_IRQ这个东西要先自己手动加到
        include/linux/interrupt.h中的那个enum中去*/

    /*这里参数的第二项就是bottom half的执行函数*/
}

/*Bottom half*/
void softirq_sample_bottom()
{
    /*做bottom half中该做的事情*/
}

/*Interrupt handler*/
static irqreturn_t softirq_sample_interrupt(int irq, void *dev_id)
{
    /* … */

    raise_softirq(SAMPLE_SOFT_IRQ);
    /*这样,就会让bottom half开始执行*/

    return IRQ_HANDLED;
}

Tasklets示例代码:

sturct tasklet_sample_device_struct { /* Pre-device structure */
    /* … */
    struct tasklet_struct tasklt;
    /* … */
}

void __init tasklet_sample_init()
{
    struct tasklet_sample_device_struct *dev_struct;
    /* … */

    /*初始化tasklet*/
    tasklet_init(&dev_struct -> tasklt, tasklet_sample_bottom, dev);
}

/* Bottom half */
void tasklet_sample_bottom()
{
    /* 做bottom half中该做的事情 */
}

/* Interrupt handler*/
static irqreturn_t tasklet_sample_interrupt(int irq, void *dev_id)
{
    struct tasklet_sample_device_struct *dev_struct;
   
    /* … */
   
    tasklet_schedule(&dev_struct -> tasklt);

    return IRQ_HANDLED;
}

关于中断处理

当中断发生的时候,操作系统会先去查找中断向量表中该中断发生的时候的中断处理程序是什么。然后再执行这个中断处理例程。

现在来看一下在内核态下(我们主要说的是驱动程序)是如何申请中断的。

经常可以在open函数的实现中看见对request_irq这个函数的调用,其实这个就是在申请中断。

假设我们已经写好了中断处理函数如下:

void interrupt_handler(int irq, void *dev_id, struct pt_regs *regs)
{
    //…
    return IRQ_HANDLED;
}

我们可以在回调函数temp_open方法中调用中断处理注册如下:

int temp_open(struct inode* inode, struct file *filp)
{
    if(!request_irq(TEMP_IRQ, interrupt_handler,

                    SA_INTERRUPT, “temp_chr”, NULL))
    {
        //…申请irq成功
    }
    return 0;
}

这样我们的中断处理函数就注册成功了,当相应的中断发生的时候,中断处理函数就会被调用。

Linux驱动程序中的I/O

在第一篇文章中只是介绍了下Linux驱动的整体结构,并没有对具体实现进行说明。

这篇文将简单介绍一下在实现Linux驱动程序中I/O操作的实现。

I/O操作最基本需要实现的函数有三个: read(), write(), ioctl().

read(), write()
一个是从硬件读数据,一个是向硬件写数据。

来看怎么与硬件交互数据:


1. I/O port读写函数:
inb, inw, inl, outb, outw, outl;

2. I/O memory读写函数:
readb, readw, readl;
writeb, writew, writel;

还是在I/O memory中的函数,这三个是一点小福利
memset_io, memcpy_fromio, memcpy_toio;
这三个东西和memset,memcpy的用法基本完全一样。

ioctl()
略,将会有一篇小文章专门说它~

User Space 与 Kernel Space的交互

请注意一下,我们写的这些函数的参数其实都是由用户程序给传过来的。

而我们现在是在内核态,内核态不能使用用户态的内存。

所以我们在写驱动程序的时候不能直接读写用户态的资源。

怎么办呢?

copy_from_user() 与 copy_to_user()

第一个函数多用于write()的实现中,它可以把用户态数据拷贝到内核态去:

ssize_t xxx_write(struct file *filp, const char *buf, size_t count, loff_t *f_pos)

用户通过给驱动程序传送const char *buf这个参数来表示要写给硬件的数据。

一般在驱动程序使用这样的用法:

copy_from_user(kernel_memory, buf, count);

这样来把buf中的数据给拷贝到kernel_memory这块内核态的内存空间中。

然后驱动程序就可以通过读出kernel_memory的数据并写入硬件就好了。

第二个函数用法和第一个差不多,多用于read()中,可以把内核态数据拷贝到用户态去

回头写一个简单的示例程序来演示一下,这样直观一些。

Linux驱动程序架构(一)

(转载请注明出处)

上一篇文章有说到用户程序调用C标准库函数的时候,会对应到驱动程序file_operations中函数指针所指向的一个回调函数上去。

现在来分析一下这个调用的过程(图片在墙外):

对于这图,我想要说的有:


1.struct file数据结构:
        它与file_operations一样,也是在/include/linux/fs.h中定义的。这是一个内核数据结构,不会出现在用户态。

        file结构其实代表的是一个被打开的文件,每一个被打开的文件在内核空间都有一个对应的file结构。在open一个文件的时候,这个结构会被创建。

        struct file中有一个元素:struct file_operations *f_op;
        在open设备文件的时候,这个f_op会被初始化为驱动程序中的file_operations

        这样,对设备文件的操作就被映射到驱动程序中的实现上去了

2. VFS(Virtual File System) Layer
        因为设备文件它并不是真正意义上的文件,只不过是起到了将system calls映射到驱动程序中的操作上罢了。所以我们把它叫做Virtual File.
        (明晰一个概念:设备文件就是VFS)
=====
话说在linux下面有没有好用的画图工具啊?类似Visio那样的。上面那个图画的我辛苦啊。

Linux驱动程序设计之入门篇

(转载请注明出处)


一. 明晰基础概念和工作方式

在说驱动之前要先明确一个概念,大家应该都知道,在linux中把一切东西都当作文件。

同样的,一个硬件也被当成一个文件,这些代表硬件的文件我们把它们叫做设备文件

设备文件被统一的放在/dev路径下面。
(用过mount命令的应该很清楚,mount就是将/dev下面的设备文件给挂载到我们的文件系统上)

写过程序的一般都很清楚,当我们打开普通文件之后可以读写,可以对文件作很多事情。

而现在我们面对的是一个设备文件,它代表的是一个硬件,那么读写之类各种操作是由什么东西来完成的呢?

答案是:驱动程序


其实驱动程序在做什么事情呢?答案很简单,就是实现c语言中那些对文件的操作函数。当用户程序调用这些操作函数如open(),read(),write()等等普通的glibc中的函数时,会自动将操作映射到驱动程序中对这些操作的实现。以此来实现对硬件的读写操作。

用户态程序对设备文件的操作的例子就像这样:

int fd;
char buff[10];
fd = open(“/dev/port”, O_RDWR);
write(fd, buff, 1);
close(fd);

大概代码就像这样。

二. 核心数据结构

现在要介绍一个在理解驱动程序设计架构时最重要的数据结构 – file_opeartions
(是我自己以为的,不同意见可以说…嘿嘿)

我不直接说这个数据结构,我用一段最简单的驱动程序的代码来说明。

int xxx_open(struct inode *inode, struct file *filp)
{
    //…
}

int xxx_release(struct inode *inode, struct file *filp)
{
    //…
}

ssize_t xxx_read(struct file *filp, char *buf, size_t count, loff_                   *f_pos)
{
    //…
}

struct file_operations xxx_fops =
{
    .owner = THIS_MODULE,
    .open = xxx_open,
    .release = xxx_release,
    .read = xxx_read;
};

一般聪明的人看了这个代码就会知道这个file_operations是个什么东西。

它定义了由驱动程序实现的一系列具体操作的函数指针。

我们写驱动程序就是去填这一个个的函数,并且把file_operations中的函数指针指向我们自己写的这些函数(我们把这种函数叫做回调函数(callback function))。

当然,file_operations中有很多很多的函数指针.

它的定义在linux的源代码的/include/linux/fs.h文件中,希望读者去看看,而且由于linux每一个版本间的变化,这个数据结构并不是不变的,很可能某一个函数指针在某一个版本就没有掉了。所以在实现自己的驱动程序之前,最好先去查看下这个代码。

三. 设备的注册与注销

代码很简单我写出来大家一看就知道

int xxx_init(void)
{
    register_chrdev(181, “temp_chr”, &xxx_fops);
    //…
}

void xxx_exit(void)
{
    unregister_chrdev(181, “temp_chr”);
    //…
}

moudle_init(xxx_init);
module_exit(xxx_exit);
//注,把上面的代码和这个代码合在一起就是一个完整的linux驱动程序。

register_chrdev这个函数的三个参数分别是:
1.指定的主设备号
2.我给设备的命名
3.file_operations结构

四. 如何编译驱动

Makefile文件就一行:
obj-m := temp_chr.o

编译命令make -C /usr/src/linux-2.6.31/ M=/temp_chr/ modules
(当然,你可以把命令直接写到Makefile文件中去,这样直接make一下就好了)

解释一下这个编译的参数.

第一个路径/usr/src/linux-2.6.31/这个是指定的linux源码存储的位置。
编译出来的驱动程序也只能适用于这个版本的linux内核。如果你要编译适用于其他版本内核的驱动程序,那么就先去下载一个那个版本的内核代码,然后把这个参数改成那个源码的路径就好了。

M=/temp_chr/这个路径是你的驱动程序的存储位置。
如果你就在该目录下面可以改成M=$(pwd)

编译好之后就会出现一个temp_chr.ko文件,这个就是我们编译好的驱动程序。

如果你的这个模块(驱动程序是以内核模块(kernel modules)存在于linux中的,那个.ko文件就是一个内核模块)包括了多个.c文件,那么Makefile文件就这样写:

obj-m := modulename.o
module-objs := file1.o file2.o

五. 如何加载驱动

命令很简单:insmod temp_chr.ko

由于我这个驱动用的是比较老的注册方式,所以不会自动创建设备文件,所以需要手动创建一个:

cd /dev
mknod temp_chr c 181 0

这样就好了,参数的意义
1.temp_chr是被创建设备文件的名字
2.c表示这个是字符设备
3.181是主设备号
4.0是次设备号

这里需要明晰一个概念:设备号

设备号由两个部分构成,主设备号次设备号

主设备号:主设备号标识了设备的类型,拥有相同主设备号的设备使用的驱动程序必定是相同的。(可以容易推出主设备号与驱动程序是一一对应的)

次设备号:次设备号区分了同一类型设备的不同实体,比如一台电脑可能有两块硬盘,一块硬盘的次设备号是1,而另一块硬盘的次设备号就绝对不是1。