日志标签 ‘驱动开发’

支持阻塞操作的字符设备驱动

2010年10月27日

前文中,我们已经知道了如何编写一个简单的字符设备驱动。本文将在此基础上为这个字符设备驱动增加阻塞功能。不过在此之前,我们会先做一些准备工作。

阻塞和非阻塞I/O

阻塞和非阻塞I/O是设备访问内核的两种不同的模式。进程以阻塞方式访问设备并对其进行操作时,如果不能及时获得I/O资源则会被挂起,直到满足可操作的条件后才进行相应的操作。这个被挂起的进程会进入睡眠状态,并被移至某个等待队列;当条件满足时候,会被移出这个等待队列。这里所说的等待队列以及相关操作在上文已说明,在此不再赘述。非阻塞方式是指进程在不能进行设备操作时并不被挂起,它要么放弃操作,要么不停进行查询,直至可以进行相关的设备操作为止。

我们现在已经知道,用户空间中的应用程序通过read()和write()等统一的系统调用来访问设备文件。而这些系统调用函数最终则会调用设备驱动中的XXX_read()和XXX_write()函数。因此,如果我们在设备驱动中实现了阻塞功能(具体会落实到某个操作函数),当应用程序进程不能及时获得设备资源时就会将该进程阻塞到资源可访问为止。那么XXX_read()和XXX_write()等函数也就不会立即返回,read()和write()等系统调用也就不会立即返回。整个阻塞-唤醒的过程用户是无法感知到的。从用户的角度来看,他们会认为直接就可以对此设备进行操作。

相反,如果设备驱动中的操作函数是非阻塞的,那么当设备资源不可用时,设备驱动中的XXX_read()和XXX_write()等函数会立即返回,那么read()和write()等系统调用也会立即返回。从用户角度来看,此时访问设备文件就出错了。

支持阻塞操作的字符设备驱动

接下来要分析的这个字符设备驱动同样使用一个全局的字符串数组globalmem来存储字符数据。XXX_read()负责将内核空间的数据(此处即为globalmem中的字符串)拷贝到用户空间,实现用户空间对设备文件的读操作;XXX_write()负责将用户空间的数据拷贝到内核空间,实现用户空间对该设备文件的写操作。另外,为了更好的演示本文所述的阻塞操作,我们对这个字符串数组globalmem进行这样的限制:当它为空时,读进程不能进行读操作;当它为满的时候,写进程不能进行写操作。当读了count字节的数据后,还要将globalmem中这些被读的数据移出这个全局数组。

如果你理解了前面那个最基本的字符设备驱动的话,除了上述的不同外,基本上没有什么地方你看不懂的。这个举例的完整代码在这里

static char globalmem[BUF_NUM];
static wait_queue_head_t rdwait;
static wait_queue_head_t wrwait;
static struct semaphore mutex;

static int len;
ssize_t myblock_read(struct file*,char*,size_t count,loff_t*);
ssize_t myblock_write(struct file*,char*,size_t count,loff_t*);

struct file_operations fops=
{
	.read=myblock_read,
	.write=myblock_write,
};

static int __init myblock_init(void)
{
	int ret;

	printk("myblock module is working..\n");

	ret=register_chrdev(MAJOR_NUM,"edsionte_block_cdev",&fops);
	if(ret<0)
	{
		printk("register failed..\n");
		return 0;
	}
	else
	{
		printk("register success..\n");
	}
	init_MUTEX(&mutex);
	init_waitqueue_head(&rdwait);
	init_waitqueue_head(&wrwait);

	return 0;
}

在内核模块加载函数中,先申请字符设备号;再初始化互斥信号量mutex;最后分别初始化了读等待队列头和写等待队列头。另外定义了一个全局变量len来记录当前globalmem中实际的字节数,而BUF_NUM则是最大长度。

在读函数中,我们先创建一个代表当前进程的等待队列结点wait,并把它加入到读等待队列当中。但这并不意味着当前进程就已经完全睡眠了,还需要调度函数的调度。我们前面已经说过,当共享数据区的数据长度为0时,就应该阻塞该进程。因此,在循环中,首先将当前进程的状态设置TASK_INTERRUPTIBLE。然后利用schedule函数进行重新调度,此时,读进程才会真正的睡眠,直至被写进程唤醒。在睡眠途中,如果用户给读进程发送了信号,那么也会唤醒睡眠的进程。

当共享数据区有数据时,会将count字节的数据拷贝到用户空间,并且唤醒正在睡眠的写进程。当上述工作完成后,会将当前进程从读等待队列中移除,并且将当前进程的状态设置为TASK_RUNNING。

关于从全局缓冲区移出已读数据,这里要特别说明一下。这里利用了memcpy函数将以(globalmem+count)开始的(len-count)字节的数据移动到缓冲区最开始的地方。

另外,在上述操作过程中,还加入了互斥信号量防止多个进程同时访问共享数据len和globalmem。

ssize_t myblock_read(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&rdwait,&wait);

	while(len==0)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

		down(&mutex);
	}

	if(count>len)
	{
		count=len;
	}

	if(copy_to_user(buf,globalmem,count)==0)
	{
		memcpy(globalmem,globalmem+count,len-count);
		len-=count;
		printk("read %d bytes\n",count);
		wake_up_interruptible(&wrwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto copy_err_out;
	}

copy_err_out:up(&mutex);
signal_out:remove_wait_queue(&rdwait,&wait);

	set_current_state(TASK_RUNNING);
	return ret;
}

在写函数中,如果检测到globalmem当前的长度是BUF_NUM,则阻塞当前的进程;否则,从用户空间将数据拷贝到内核空间。写函数的控制流程大致与读函数相同,只不过对应的等待队列是写等待队列。

ssize_t myblock_write(struct file*fp,char*buf,size_t count,loff_t*offp)
{
	int ret;
	DECLARE_WAITQUEUE(wait,current);

	down(&mutex);
	add_wait_queue(&wrwait,&wait);

	while(len==BUF_NUM)
	{
		__set_current_state(TASK_INTERRUPTIBLE);
		up(&mutex);
		schedule();
		if(signal_pending(current))
		{
			ret=-1;
			goto signal_out;
		}

         	down(&mutex);
	}
	if(count>(BUF_NUM-len))
	{
		count=BUF_NUM-len;
	}

	if(copy_from_user(globalmem+len,buf,count)==0)
	{
		len=len+count;
		printk("written %d bytes\n",count);
		wake_up_interruptible(&rdwait);
		ret=count;
	}
	else
	{
		ret=-1;
		goto COPY_ERR_OUT;
	}

signal_out:up(&mutex);
COPY_ERR_OUT:remove_wait_queue(&wrwait,&wait);
	set_current_state(TASK_RUNNING);

	return ret;
}

上述就是支持阻塞模式的字符设备驱动。关于上述程序更多的解释如下:

1.两种睡眠。当读进程读数据时,如果发现写进程正在访问临界区,那么它会因为不能获得互斥信号量而阻塞;而当读进程获得信号量后,如果当前globalfifo的数据数为0,则会阻塞。这种阻塞是由我们在设备驱动中实现的。

2.两种唤醒。当写进程离开临界区并释放信号量时,读进程会因信号量被释放而唤醒;当写进程往globalfifo中写入了数据时,读进程会被写进程中的唤醒函数所唤醒。特别的,如果读进程是以轻度睡眠方式睡眠的,那么用户可以通过发送信号而唤醒睡眠的读进程。

3.唤醒后如何执行。无论因哪种方式而睡眠,当读进程被唤醒后,均顺序执行接下来的代码。

4.down操作和add_wait_queue操作交换。在原程序中,读进程先获取信号量,再将读进程对应的等待队列项添加到读等待队列中。如果交换,当读进程的等待队列项加入到等待队列后,它可能又会因未获得信号量而阻塞。

5.up操作和remove_wait_queue操作交换。这两个操作分别对应标号out和out2。如果读进程从内核空间向用户空间拷贝数据失败时,就会跳转到out。因为读进程是在获得信号量后才拷贝数据的,因此必须先释放信号量,再将读进程对应的等待队列项移出读等待队列。而当读进程因信号而被唤醒时,则直接跳转到out2。此时读进程并没有获得信号量,因此只需要移出队列操作即可。如果交换上述两个操作,读进程移出等待队列时还未释放互斥信号量,那么写进程就不能写。而当读进程因信号而唤醒时,读进程并没有获得信号量,却还要释放信号量。

通过下述方法,你就可以体验到以阻塞方式访问设备文件。

1.make编译文件,并插入到内核;
2.创建设备文件结点:sudo mknod /dev/blockcdev c major_num 0;
3.修改设备文件权限:sudo chmod 777 /dev/blockcdev;
4.终端输入:cat /dev/blockcdev&;即从字符设备文件中读数据,并让这个读进程在后台执行,可通过ps命令查看到这个进程;
5.中断继续输入:echo ‘I like eating..’ > /dev/blockcdev;即向字符设备文件中写入数据;

通过上述的步骤,可以看到,每当利用echo命令写入数据时,后台运行的读进程就会读出数据,否则读进程一直阻塞。此外,如果你愿意的话,还可以分别编写一个读写进程的程序进行测试。

你的第一个中断程序!

2010年10月2日

Last Update:2011/11/09

本文通过一个简单的中断程序来描述一般中断程序的基本框架。完整代码这里

中断程序一般会包含在某个设备的驱动程序中,因此,接下来的程序本质上还是一个内核模块。说到内核模块,你应该知道首先去看什么了吧?对了,就是内核模块加载函数。

static int __init myirq_init()
{
	printk("Module is working..\n");
	if(request_irq(irq,myirq_handler,IRQF_SHARED,devname,&mydev)!=0)
	{
		printk("%s request IRQ:%d failed..\n",devname,irq);
		return -1;
	}
	printk("%s rquest IRQ:%d success..\n",devname,irq);
	return 0;
}

在内核加载函数中,我们除了显示一些信息外,最重要的工作就是申请一根中断请求线,也就是注册中断处理程序。很明显,这一动作是通过request_irq函数来完成的。这个函数的原型如下:

static int request_irq(unsigned int irq, irq_handler_t handler, unsigned long flags,const char *name, void *dev);

第一个参数是中断号,这个中断号对应的就是中断控制器上IRQ线的编号。

第二个参数是一个irq_handler_t类型个函数指针:

typedef irqreturn_t (*irq_handler_t)(int, void *);

handler所指向的函数即为中断处理程序,需要具体来实现。

第三个参数为标志位,可以取IRQF_DISABLED、IRQF_SHARED和IRQF_SAMPLE_RANDOM之一。在本实例程序中取IRQF_SHARED,该标志表示多个设备共享一条IRQ线,因此相应的每个设备都需要各自的中断服务例程。一般某个中断线上的中断服务程序在执行时会屏蔽请求该线的其他中断,如果取IRQF_DISABLED标志,则在执行该中断服务程序时会屏蔽所有其他的中断。取IRQF_SAMPLE_RANDOM则表示设备可以被看做是事件随见的发生源。

第四个参数是请求中断的设备的名称。可以在/proc/interface中查看到具体设备的名称,与此同时也可以查看到这个设备对应的中断号以及请求次数,甚至中断控制器的名称。

第五个参数为一个指针型变量。注意此参数为void型,也就是说通过强制转换可以转换为任意类型。这个变量在IRQF_SHARED标志时使用,目的是为即将要释放中断处理程序提供唯一标志。因为多个设备共享一条中断线,因此要释放某个中断处理程序时,必须通过此标志来唯一指定这个中断处理程序。习惯上,会给这个参数传递一个与设备驱动程序对应的设备结构体指针。关于中断程序,可参考这里的文章。

以上就是request_irq函数各个参数的意义。

与中断处理程序的注册相对应的是free_irq函数,它会注销相应的中断处理程序,并释放中断线。这个函数一般被在内核模块卸载函数中被调用。

static void __exit myirq_exit()
{
	printk("Module is leaving..\n");
	free_irq(irq,&mydev);
	printk("%s request IRQ:%d success..\n",devname,irq);
}

如果该中断线不是共享的,那么该函数在释放中断处理程序的同时也将禁用此条中断线。如果是共享中断线,只是释放与mydev对应的中断处理程序。除非该中断处理程序恰好为该中断线上的最后一员,此条中断线才会被禁用。在此处,你也可以感受到mydev的重要性。

下面具体分析中断处理函数。该函数的功能很简单,只是显示一些提示信息。

static irqreturn_t myirq_handler(int irq,void* dev)
{
	struct myirq mydev;
	static int count=1;
	mydev=*(struct myirq*)dev;
	printk("key: %d..\n",count);
	printk("devid:%d ISR is working..\n",mydev.devid);
	printk("ISR is leaving..\n");
	count++;
	return IRQ_HANDLED;
}

另外,本内核模块在插入时还需要附带参数,下面的语句首先定义两个参数,然后利用宏module_param宏来接受参数。

static int irq;
static char* devname;

module_param(devname,charp,0644);
module_param(irq,int,0644);

使用方法:

1.通过cat /proc/interrupts查看中断号,以确定一个即将要共享的中断号。本程序因为是与键盘共享1号中断线,因此irq=1;

2.使用如下命令就可以插入内核:

sudo insmod filename.ko irq=1 devname=myirq

3.再次查看/proc/interrupts文件,可以发现1号中断线对应的的设备名处多了myirq设备名;

4.dmesg查看内核日志文件,可看到在中断处理程序中所显示的信息;

5.卸载内核模块;

可以看到,内核模块加载后,我们所写中断处理程序是被自动调用的,主要是因为该中断线上有键盘所发出的中断请求,因此内核会执行该中断线上的所有中断处理程序,当然就包括我们上述所写的那个中断处理程序。关于中断处理程序的执行,可参考这里的文章。

这样,一个最基本的中断程序就编写完成了!try!

后记:

这个程序调试起来并不难,但是我们并不能仅仅局限在这个程序本身。以它为入口点深入学习中断的基本原理再好不过。下面给出几个学习的入口点。

1.为何我们的中断程序和其他设备共享了一个中断线后会被执行?或者说,共享中断线上的所有中断服务例程是怎么执行的?

2.中断涉及到那些基本的数据结构?这些数据结构之间有什么关系?

3.do_IRQ()函数的大体执行流程是什么?

亲们,要学习的东西还很多,让我们一起加油吧!

什么是中断?

2010年10月1日

Last Update:2011/11/03

我们经常听到中断这个词,到底什么是中断?在这之前我先讲给大家一个故事。

从前有两位班主任A和B,A老师带一班,B老师带二班。这两位老师平时都很忙,平时除了为学生们备课改作业,当某个同学提出问题时,还要为他们解答疑问。A老师生怕遗漏每一位同学提出的问题,每隔一段时间就放下手头的工作,不断轮流寻问每一位同学:“你有问题吗”。也许被寻问的这位同学恰好有问题要咨询老师,可这毕竟是少数;而当A老师继续批改作业的时候又出现了一些同学提出问题,可是这个时候还没到A老师轮流寻问同学的时间。就这样,A老师的宝贵时间经常被浪费。

B老师和A老师有所不同,他认为完全没有必要这样死板的寻问每一位同学是否有问题。他对全班同学说:“谁有问题就主动来找我“。即便他可能正在改作业,但是完全可以暂时放下手头那些重要的工作,先为这位迷惑的同学解决问题。因此,B老师既可以改作业,又可以在学生主动提出问题的时候为那个学生解决问题。显然B老师的工作效率比A老师提高了很多。

故事看懂了,那么恭喜你,你也懂了什么是中断。

上面的两位老师分别代表系统中对设备进行管理的两种典型的方式。A老师的那种工作方式属于早期的程序查询控制方式(或称为轮询),内核定期对设备的状态进行查询;而B老师则属于中断控制方式,I/O设备需要服务时,可主动向内核发出中断请求并打断CPU当前正在执行的任务。前者内核为主动;而后者变内核主动为被动,由设备主动向内核发出中断请求。

从物理角度来看,中断请求是由外部硬件设备产生的一种电信号,外部设备首先将这种电信号发给中断控制器,接着中断控制器将此电信号发送给CPU。CPU检查到该中断信号后再通知内核,然后由内核完成后续的一些列处理工作。显然,内核不需要定期去检查设备,从而提高了CPU利用率。

通过以上的描述,你应该对中断有了一个大致的了解,不过想要更具体的了解中断必须搞清楚以下的知识点。

1.中断的分类

早期以及一般情况下,我们所说的中断即指由外设所产生的中断。随着计算机的迅速发展,中断不再仅仅局限于外部设备,CPU本身也会产生中断,不过我们将这种中断称为异常。

对于x86体系结构而言,中断可以分为两大类:同步中断和异步中断。同步中断即我们上面所说的异常,它是由 CPU 在执行非法命令时所产生的。之所以称为同步,是因为这种中断请求信号与代码指令同步执行,也就是说只有在一条指令执行完毕后 CPU 才会发出中断,而不是发生在代码指令执行期间。而异步中断即由外部设备产生的中断,这种中断可以随时发生,习惯上,我们将异步中断仍然称为中断。

中断可分为可屏蔽中断(Maskable interrupt)和不可屏蔽中断(Nomaskable interrupt)。异常可分为故障(fault)、陷阱(trap)、终止(abort)三类。

可屏蔽中断主要是针对外部设备所产生的中断信号,不可屏蔽中断一般是指计算机内部硬件产生的中断。由于异常是CPU发出的中断信号,与中断控制器(下文有解释)无关,因此异常不能被屏蔽。那么,异常和不可屏蔽中断有些相似点:它们均与外部设备无关,并且均不能被屏蔽。

2.中断控制器

中断控制器可分为可编程中断控制器(Programmable Interrupt Controller,PIC)和高级可编程中断控制器(Advanced Programmable Interrupt Controller,APIC)。前者仅可用于单处理器(Uni-processor)平台,后者则可用于多处理器(Mutiliple Processor)平台。

传统的PIC都是通过两片级联的8259A来管理和控制15个由外部设备所产生的中断源。由下图可看到,每片8259A芯片最多可管理8个中断源。但由于两片8259A芯片的级联,即从片的INT输出端与主片的2号(第三条线)中断线相连接,所以总共可以管理15个中断源。

外设和中断控制器上每根相连的线被称为中断线(也称为IRQ线)。对这些中断线进行编号就形成了中断号。IRQn线所对应的中断号即为n。当外部设备产生中断时,就通过中断线向系统发出中断请求(Interrupt ReQuirement)。由于中断控制器只能控制15个中断源,而如今的外设又日益剧增,因此就出现了对各外设共享一条中断线的情况,即中断共享(后文会有详细解释)。

上面已经说过,外部设备的中断请求可以被屏蔽,但是具体体现到硬件上是如何做到的?在每个8259A芯片上都有一个8位的中断屏蔽寄存器,每一位对应一条中断线。在对应为上置1则可屏蔽此条中断线;相反置0则可启动此条中断线。

需要说明的是,现在大多数计算机都使用的是APIC,可以通过查看/proc/interrupts文件获取中断控制器的名称。之所以将传统的8259A作为PIC的举例来学习,最大的原因还是因为它非常的经典,就如同古董——8086处理器一样。通过查看/proc/interrupts文件,可以发现APIC的中断号并不止15个(但即便是这样,还需要共享中断)。

3.中断向量

x86体系结构支持256种中断,即256个中断源。将这些中断源按照0到255的顺序对每种中断进行编号,这个编号叫做中断向量,通常使用8位无符号整数来存储这个向量。中断号和中断向量存在一对一的映射关系。

中断号和中断向量是两个不同的概念。当I/O设备把中断信号发送给中断控制器时,与之关联的是一个中断号;而当中断控制器将该中断信号传递给CPU时,与之关联的是一个中断向量。也就是说,中断号是以中断控制器的角度而言的;而中断向量则是以CPU的角度而言的。中断号和中断向量存在一对一的映射关系。

通常,Intel将编号为0~31的向量分配给异常和非屏蔽中断,这部分向量是固定的。因此在8259A默认情况下,中断号n所对应的中断向量为n+32。

4.中断服务例程

在响应一个具体的中断时,内核会执行一个函数,该函数被称为中断服务例程(Interrupt Service Routine,ISR)。每一个设备的驱动程序中都会定义相关的中断服务例程。从下面的代码可以看到,中断服务例程有两个参数,分别为int型和void指针型。并且返回值为irqreturn_t。

 //linux/include/linux/interrupt.h
  98typedef irqreturn_t (*irq_handler_t)(int, void *);
 //linux/include/linux/irqreturn.h
  10enum irqreturn {
  11        IRQ_NONE,
  12        IRQ_HANDLED,
  13        IRQ_WAKE_THREAD,
  14};
  15
  16typedef enum irqreturn irqreturn_t;

由于irqreturn_t是一个枚举类型,因此本质上为整形。并且IRQ_NONE,IRQ_HANDLED,IRQ_WAKE_THREAD的值一次为0,1,2。IRQ_NONE表示不处理所收到的中断请求;IRQ_HANDLED表示接收到了有效的中断请求,并且作出了正确的处理(这一点在后文中有详细讨论)。

需要注意的是,当一个中断服务例程正在执行的时候,该中断所在所有处理器上的都会被屏蔽,以免继续接受同一条中断线上的另一个新的中断。

以上就是关于中断的基本概念,了解了它们就容易进行后续的中断分析了。

字符设备驱动分析(1)

2010年9月16日

Last Update:9/24(斜体字为更新内容)

Last Update:9/20

Last Update:9/19

熟悉了模块编程的基本框架后,我们就可以试着分析一个简单的字符设备驱动。下面以《设备驱动开发详解》一书中的代码6.17为例来分析这个字符设备驱动的代码。

我们现在对于对前文中hello,kernel内核模块进行稍微的改动。我们都知道内核模块的入口函数是module_init(function name)内注册的函数。也就是告诉内核“从这个函数入口”。那么我们分析字符设备驱动模块,首先应该去看globalmem_init函数。

module_init(globalmem_init);
module_exit(globalmem_exit);

在globalmem_init函数中,首先通过宏MKDEV获得32位的设备驱动号。通常linux中的设备号由主设备号和次设备号组成,主设备号对应每一类设备,而次设备号对应该类设备的具体一个设备。dev_t类型前12位是主设备号(但事实上主设备号的前四位被屏蔽了,如果去看源码就可得知),后20位为次设备号。由于可以事先指定主设备号globalmem_major,因此我们需要用宏MKDEV来获得dev_t类型的设备号:

#define MINORBITS       20
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

这个宏通过移位和或运算,巧妙的得到dev_t类型的设备号。这个宏可以在include/linux/kdev_t.h中查找到。网上有的资料中会给出MINORBITS为8,这个应该是适合16位的设备号的情况。

接着通过全局变量globamem_major来判断是否事先分配了起始的设备号。如果是,则继续分配连续的一段设备号,否则动态分配设备号,并且通过MAJOR宏获得分配以后的主设备号。这里需要强调的是,下面的两种设备号分配函数,都是一次性分配一组连续的设备号(当然也可以只分配一个设备号,调整参数即可)。

首先我们分析已知起始设备号的情况。通过调用register_chrdev_region函数,便可以申请到一组连续范围的设备号。在linux/fs/char_dev.c中可以看到此函数的原型。

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

其中from是首设备号,而count是这组连续设备号的数目。name为设备名。而《设备》一书中的count为1,也就是说,这组设备号的数量为1。

其次,当起始设备号并未指定时就要动态的申请了,使用下面的函数:

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

与上面静态分配函数不同的是,此时dev是一个指针类型,因为要返回将要分配的设备号。关于以上两个函数的内核源码分析,具体参见这里

以上都成功执行后,我们给globalmem_devp指针分配内存空间。其中globalmem_devp是一个指向globalmem设备结构体的指针。我们通过mmset对此块内存空间进行初始化之后。接着globalmem_setup_cdev函数对cdev初始化并将此字符设备注册到内核。

//设备驱动模块加载函数
int globalmem_init(void)
{
	int result;
	dev_t devno=MKDEV(globalmem_major,0);

	if(globalmem_major)
	{
		result=register_chrdev_region(devno,1,"globalmem");
	}
	else
	{
		result=alloc_chrdev_region(&devno,0,1,"globalmem");
		globalmem_major=MAJOR(devno);
	}
	if(result<0)
		return result;
	globalmem_devp=kmalloc(sizeof(struct globalmem_dev),GFP_KERNEL);
	if(!globalmem_devp)
	{
		result=-ENOMEM;
		goto fail_malloc;
	}

	memset(globalmem_devp,0,sizeof(struct globalmem_dev));

	globalmem_setup_cdev(globalmem_devp,0);
	return 0;
fail_malloc:unregister_chrdev_region(devno,1);
	return result;
}

上面的程序已经很“整齐”的说明了init函数的主要作用。具体如下:

1.申请设备号

2.为设备相关的数据结构分配内存

3.初始化并注册cdev(globalmem_setup_cdev函数实现)

有了字符设备驱动的加载函数,那么肯定有卸载函数:

void globalmem_exit(void)
{
cdev_del(&globalmem_devp->cdev);
kfree(globalmem_devp);
unregister_chrdev_region(MKDEV(globalmem_major, 0), 1);
}

总体来看,globalmem_init函数完成的是字符设备的一些初始化工作,以及向系统内注册。而globalmem_exit就是进行字符设备的释放工作:从内核中删除这个字符设备,释放设备结构体所占的内存,以及释放申请的设备号。从结构上看,它并没有偏离内核模块编程的结构范围,仍然是我们熟悉的hello,kernel。

我们在此先暂时将globalmem_setup_cdev函数内部的实现用return 0;来代替。那么我们现在用插入内核模块的命令将我们这个字符设备驱动(尽管许多功能还未实现)插入到内核中。成功后我们可以通过cat /proc/devices 命令来查看刚刚字符设备名称以及主设备号。/proc是一个虚拟的文件系统,这里的虚拟是指这个文件系统并不占用磁盘空间而只存在于内存中。而该目录下的devices文件中存储着系统字符和块设备的驱动名称以及设备编号。

接下来,我们可以通过:mknod /dev/globalmem c 250 0命令创建一个设备节点。有了这个设备节点后,就可以对它进行类似普通文件那样的操作了(当然现在还不能,因为并未实现具体的操作函数)。

这样一个字符驱动的大致上有了雏形。

对Makefile、Kconfig与.config文件的再次理解

2010年9月12日

虽然前文中对Makefile、Kconfig以及.config三个文件又过解释,但是在做过几个简单的例子后,对这三个文件有了更深入的理解,(本文参考了苏锦秀师姐的PPT)现在总结如下:

1.我们要在内核中增加程序(比如驱动程序),并且使这个驱动程序能够编译进内核,基本分为两大部分。首先我们要告诉内核“请您下次编译的时候捎带上我”,即需要我们进行内核的相关配置,这就需要对相关Makefie和Kconfig文件进行修改,以便让内核知道将要对这个新的驱动程序进行编译。而仅仅只告诉内核“我需要你编译我”还不行,更重要的是让内核真正的去“行动”,即编译内核。

2.Makefile文集是整个内核工程编译命令的集合。它根据配置情况,构造出需要编译的内核源码文件列表,然后分别编译,并把目标代码链接到一起,形成内核二进制文件。也就是说Makefile只是存储了源码文件构建目标文件的规则,具体是否按着规则去执行还要看那些配置变量。

3.我们进行make menuconfig时,会出现一个配置菜单,它是由各层Kconfig文件组成。Kconfig文件是以分布式的方式位于源码的各个子目录当中。最底层的Kconfig位于源码目录下的arch/x86/Kconfig。由此入口,使用source语句把需要的子Kconfig文件加入到上级目录的Kconfig中,以此递归下去。Kconfig文件控制配置菜单是否出现新驱动的配置选项。用户通过Kconfig文件产生的配置选项,来控制对新驱动的配置。

4.我们在配置菜单中进行的相关配置(【】,【*】,【M】),最终都会存储于.config文件当中,因此Kconfig文件跟这些配置结果并没 有直接的关系,只是提供了配置菜单中的配置选项。

windows 7 ultimate product key

windows 7 ultimate product key

winrar download free

winrar download free

winzip registration code

winzip registration code

winzip free download

winzip free download

winzip activation code

winzip activation code

windows 7 key generator

windows 7 key generator

winzip freeware

winzip freeware

winzip free download full version

winzip free download full version

free winrar download

free winrar download

free winrar

free winrar

windows 7 crack

windows 7 crack

windows xp product key

windows xp product key

windows 7 activation crack

windows7 activation crack

free winzip

free winzip

winrar free download

winrar free download

winrar free

winrar free

download winrar free

download winrar free

windows 7 product key

windows 7 product key