存档在 ‘驱动开发’ 分类

字符设备驱动再学习

2011年1月15日

本学期一直在学习linux下到设备驱动开发,字符设备驱动是设备驱动开发中最基本和重要的一部分。前几天的考试让我意识到对这部分的内容理解的还不是很清楚,因此,很有必要再次理解学习字符设备驱动。

本文以全局内存字符设备globalmem为例,说明字符设备驱动的结构以及编写方法。

1.字符设备的数据结构

在linux内核中使用struct cdev来表示一个字符设备,如下:

//在linux/include/linux/cdev.h中
  12struct cdev {
  13        struct kobject kobj;
  14        struct module *owner;
  15        const struct file_operations *ops;
  16        struct list_head list;
  17        dev_t dev;
  18        unsigned int count;
  19};

下面对该数据结构的字段作简单解释:

owner:该设备的驱动程序所属的内核模块,一般设置为THIS_MODULE;
ops:文件操作结构体指针,file_operations结构体中包含一系列对设备进行操作的函数接口;
dev:设备号。dev_t封装了unsigned int,该类型前12位为主设备号,后20位为次设备号;

cdev结构是内核对字符设备驱动的标准描述。在实际的设备驱动开发中,通常使用自定义的结构体来描述一个特定的字符设备。这个自定义的结构体中必然会包含cdev结构,另外还要包含一些描述这个具体设备某些特性到字段。比如:

struct globalmem_dev
{
	struct cdev cdev; /*cdev struct which the kernel has defined*/
	unsigned char mem[GLOBALMEM_SIZE]; /*globalmem memory*/
};

该结构体用来描述一个具有全局内存的字符设备。

2.分配和释放设备号

在linux中,对于每一个设备,必须有一个惟一的设备号与之相对应。通常会有多个设备共用一个主设备号,而具体每个设备都唯一拥有一个次设备号。总的来看,每个设备都唯一的拥有一个设备号。前面已经提到,内核使用dev_t类型来表示一个设备号,对于设备号有以下几个常用的宏:

//在linux/include/linux/kdev_t.h中
7#define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))
8#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
9#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

上述三个宏的功能分别为:通过设备号获取主设备号,通过设备号获取次设备号,通过主次设备好获取设备号。

在设备驱动程序中,一般会首先向系统申请设备号。linux中设备号的申请都是一段连续的设备号,这些连续的设备号都有共同的主设备号。设备号的申请有两种方法,若提前设定了主设备号则再接着申请若干个连续的次设备即可;若未指定主设备号则直接向系统动态申请未被占用到设备号。由此可以看出,如果使用第一种方法,则可能会出现设备号已被系统中的其他设备占用的情况。

上出两种申请设备号的方法分别对应以下两个申请函数:

  //在linux/fs/char_dev.c中
 196int register_chrdev_region(dev_t from, unsigned count, const char *name)
 232int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count,
 233                        const char *name)

上述两个函数都可以申请一段连续的设备号。前者适用已知起始设备号的情况(通过MADEV(major,0)可以获得主设备号为major的起始设备号);后者使用于动态申请设备号的情况。如果想申请一个设备号,则将函数中的参数count设为1即可。关于这两个函数的详细源码分析,可参考这里

3.Linux字符设备驱动的组成

实现一个基本的字符设备驱动需要完成以下几部分:字符设备驱动模块的加载卸载函数和实现file_operations结构中的成员函数。

3.1.file_operations结构体

file_operations结构体中包含许多函数指针,这些函数指针是字符设备驱动和内核的接口。,实现该结构中的这些函数也是整个字符设备驱动程序的核心工作。file_operations结构中的每个函数都对应一个具体的功能,也就是对设备的不同操作。不过,这些函数是在内核模块中实现的,最终会被加载到内核中和内核一起运行。因此,用户态下的程序是不能直接使用这些函数对相应设备进行操作的。

学过系统调用后,你就会知道,比如当应用程序通过系统调用read对设备文件进行读操作时,最终的功能落实者还是设备驱动中实现的globalmem_read函数。而将系统调用read和globalmem_read函数扯上关系的则是struct file_operations。具体的操作是:

static const struct file_operations globalmem_fops =
{
	.owner = THIS_MODULE,
	.read = globalmem_read,
	.write = globalmem_write,
	.open = globalmem_open,
	.release = globalmem_release,
};

3.2.实现加载和卸载函数

由于字符设备驱动程序是以内核模块的形式加载到内核的,因此该程序中必须有内核模块的加载和卸载函数。通常,字符设备驱动程序的加载函数完成的工作有设备号的申请、cdev的注册。具体的过程可参考下图:

globalmem_init流程图(点击看大图)

从上述的图中可以看到,在内核模块加载函数中主要完成了字符设备号的申请。将字符设备注册到系统中是通过加载函数中的globalmem_setup_cdev函数来完成的。该函数具体完成的工作可以参考下图:

globalmem_setup_cdev流程图

结合上图,接下来参看globalmem_setup_cdev函数的具体代码。由cdev_init中,除了初始化cdev结构中的字段,最重要的是将globalmem_fops传递给cdev中的ops。

static void globalmem_setup_cdev(struct globalmem_dev *dev, int index)
{
	int ret;
	int devno = MKDEV(globalmem_major, index);

	cdev_init(&dev->cdev, &globalmem_fops);
	dev->cdev.owner = THIS_MODULE;
	dev->cdev.ops = &globalmem_fops;
	ret = cdev_add(&dev->cdev, devno, 1);
	if(ret){
		printk("adding globalmem error");
	}
}

通过上述的几步,就可以完成字符设备驱动加载函数。对于字符设备卸载函数而言,所作的工作就是加载函数功能的逆向:将cdev从系统中注销;释放设备结构体所占用的内存空间;释放设备号。具体可参看代码:

static void __exit globalmem_exit(void)
{
	/*free struct cdev*/
	cdev_del(&dev->cdev);
	/*free the memory of struct globalmem_dev*/
	kfree(dev);
	/*free the devno*/
	unregister_chrdev_region(MKDEV(globalmem_major,0), 1);
}

3.3.对file_operaions成员函数的实现
最基本的成员函数包括open、release、read和write等函数。对这些函数的具体实现还要根据具体的设备要求来完成。在本文所述的全局内存字符设备驱动中,我们要实现的是功能是在用户程序中对这字符设备中的这块全局内存进行读写操作。读写函数的具体功能可参考下图:

对于open和release可以不做具体实现,当用户态程序打开或释放设备文件时,会自动调用内核中通用的打开和释放函数。

这样,一个基本的字符设备驱动程序就完成了。本文所述实例是一个有代表性的通用模型,可以在理解本程序的基础上继续增加其他功能。

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

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命令写入数据时,后台运行的读进程就会读出数据,否则读进程一直阻塞。此外,如果你愿意的话,还可以分别编写一个读写进程的程序进行测试。

字符设备驱动分析(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命令创建一个设备节点。有了这个设备节点后,就可以对它进行类似普通文件那样的操作了(当然现在还不能,因为并未实现具体的操作函数)。

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

在内核中新增驱动代码目录(2)

2010年9月9日

Step by Step

上文中,我们已经理解了Makefile与Kconfig的作用,那么我们现在要在内核中增加edsionteDriver驱动代码,并告诉内核“请您下次编译的时候捎带上我”。具体应该如何来做?首先应该在Makefile中添加相关驱动文件的编译信息,然后还得在Kconfig中添加这个新驱动对应的配置选项。

好了,开始吧!

我们首先在内核根目录下添加edsioteDriver驱动目录。如何将此驱动目录与上一级目录连接起来?那么就需要我们来做一些修改。在驱动目录的上一级目录中,我们找到Makefile文件,然后在在此文件中添加下面代码(最好在自己添加的代码前后加上注释,以便与原有代码区别):

#edsionteDriver 's starting
obj-y+=edsionteDriver/
#edsinteDriver's ending

这句话使得kbulid会将edsionteDriver目录列入到向下迭代的目标当中,通俗一点说就是在编译过程中会将我们写的这个驱动目录列入编译的范围之内。obj-y文件列表将会被链接到built-in.o,并最终编译到vmlinux的目标文件列表 。这个解释属于官方说法,事实上当前我也不是很清除这是神马意思,不过你只需了解obj-y开始的文件列表会被编译并连接至内核就可以了。

我们上文中所说obj-$(CONFIG_xxx):=(文件列表)这样的语句是定义变量,其中具体规则是:变量名 变量符 变量值。变量符可以是=,+=,:=,?=中的一个,其中+=是追加赋值符,实现对变量的追加赋值。其中$(CONFIG_XXX)是对CONFIG_XXX的引用,CONFIG_XXX的值会根据依赖关系以及用户输入来决定。比如如果是depend on依赖的话,那么用户可选择y或n;如果是上文中的tristate那么就是三态选项,即y、n或m。obj-m表示会将文件当作内核模块来编译,obj-n就可以忽略不进行处理。

注意:我们刚添加的语句只是对edsionteDriver目录起作用,至于这个目录下的各个子文件是要模块编译还是链入你和还要具体看我们edsionteDriver目录下的Makefile文件。

下面修改驱动目录子目录的Kconfig文件:

#edsionteDriver 's starting
source "drivers/edsionteDriver/Kconfig"
#edsinteDriver's ending

这条脚本是将edsionteDriver目录中的Kconfig加入到上级目录中的Kconfig中。这样上级目录的Kconfig会引用到edsionteDriver目录中的Kconfig文件。通过这条语句我们也可以体会Kconfig的另一个作用:Kconfig文件将分布在当前目录下各个子目录的Kconfig都集中在自己的Kconfig当中。

通过上述两步修改就会让父目录感知到edsionteDriver的存在了。接下来我们来编写edsionteDriver目录以及其子目录下的各个Kconfig和Makefile文件。首先是edsionteDriver目录下的两个文件配置:

Makefile文件如下:

# drivers/edsionteDriver/Makefile
# just a test

obj-$(CONFIG_MYDRIVER) +=mydriver.o
obj-$(CONFIG_MYDRIVER_USER) +=mydriver_user.o
obj-$(CONFIG_KEY) +=key/

我们需要用户来选择是否编译mydriver.c,所以需要用CONFIG_MYDRIVER变量来保存选项值。下面两句类似我们一开始在父目录下增加edsionteDriver目录。
Kconfig文件如下:

# dirvers/edsionteDriver/Kconfig

menu "Test edsionteDriver"

config MYDRIVER
    tristate "mydriver test!"
config MYDRIVER_USER
    bool "user-space test"
    depends on MYDRIVER

source "drivers/edsionteDriver/key/Kconfig"
endmenu

首先menu和endmenu之间代码创建了一个菜单Test edsionteDriver,然后第一条config语句会建立一个名为”mydriver test”的配置菜单,用户在尽享此项目的配置时输入的配置信息(Y,N或M),会存储在config后面的MYDRIVER变量中。而这个变量会直接关系到Makefile文件中是否编译相应的文件。第二个config语句还是创建一个配置菜单的条目,只不过比较特殊的是这个菜单依赖于上面我们创建的菜单“mydriver test”(语法上是MYDRIVER_USER依赖于MYDRIVER)。具体含义是,只有当用户选择配置“mydriver test”时,才会出现下级菜单”user-sapce test”;否则这个下级菜单是不会出现的。

最后一条source语句的作用是将edsionteDriver目录下的子目录key/也加入到内核编译时扫描的对应当中。

接下来,就应该修改key目录下的相应文件了,这里不再详细说明,如果你成功修改了上述文件,修改key目录下的文件应该不困难吧?

这样的step by step并不是为了教会各位如何产生那个配置菜单,而是让各位在基本理解Kconfig,Makefile以及.config三者的基础上更深入的去学习。

在内核中新增驱动代码目录(1)

2010年9月8日

Step by Step

如果学习Linux下驱动开发,那么本文所述的“在内核中新增驱动代码目录”应该是一个最基本的知识点了。那么如何将自己写好的驱动程序新增到内核?本文将一步一步的教会你。

1.在正式开始之前,请先切换到root用户:su root。不过可能会会出现问题:不管你输入什么密码,都会提示你错误(很可能是因为之前你根本未设置过密码)。这时候我们来修改root用户的密码:

sudo passwd root

输入两次后,即可修改完毕,这下再su root就可以成功切换到root用户。

2.你可以现在试着在终端输入make menuconfig,终端会提示你:make: *** 没有规则可以创建目标“menuconfig”。这是因为menuconfig涉及到图形界面,所以我们得安装一些依赖包(ubuntu下):sudo apt-get install libncurses5-dev。

3.在一般的教程中,都会提到.config文件,而且这个文件就位于内核代码的根目录下。因此我会输入命令:ls -a来寻找.config。可是找来找去都没有这个文件的踪影。这是为什么?这是因为在这之前,你从来没有进行过内核配置,所以当然就不会生成.config文件了。解决的方法也很简单,有了上面两步的准备工作,那么你应该会成功进入配置用户界面,然后什么也不做,保存退出即可。那么你再ls一下,你可以发现.config已经存在了。

在开始向加入驱动代码之前,我们先了解三项基本步骤:

(1)将编好的源代码复制到Linux内核源代码的相应目录

(2)在目录的Kconfig文件中增加新源代码对应项目的编译配置选项

(3)在目录的Makefile文件中增加对新源代码的编译条目

在完成上述三项工作之前,我们先看一下我们要新增的驱动的树形结构。比如我们写的驱动程序均放在edsionteDriver目录,在此目录中包含Kconfig,Makefile和test.c三个文件,以及Key和led两个目录。我们先提前创建好这些文件,请注意本文只是为了演示说明,如果实际应用,像key,led以及test.c这样的文件都是有实际意义的。那么现在复制到内核源码目录下的driver/目录下即可。

|– edsionteDriver
|    |– Kconfig
|    |– Makefile
|    |– key
|        |– Kconfig
|        |– Makefile
|        |– mykey.c
|    |– mydriver.c
|    |– mydriver_user.c

现在我们完成了第一步工作,你应该注意到,我们现在只是创建了各个目录下的Kconfig和Makefile文件,并没添加相关内容,所以接下来我们就来进行这两个文件的编写。

对于初学者来说,直接学习Makefile以及Kconfig的编写可能会有些眩晕甚至排斥学习,不过我们可以先了解这两个文件在实际的内核分析中有什么作用。一般来说,对于内核这个庞大的网络,想要快速定位你所关心的代码就需要首先分析某个目录下的Makefile以及Kconfig文件,它们可是我们分析内核代码的goole map。

比如我们要分析ext3类型的文件系统,那么我们进入源码目录下fs/ext3/目录中,我们打开此目录的Makefile文件:

  1 #
  2 # Makefile for the linux ext3-filesystem routines.
  3 #
  4
  5 obj-$(CONFIG_EXT3_FS) += ext3.o
  6
  7 ext3-y  := balloc.o bitmap.o dir.o file.o fsync.o ialloc.o inode.o \
  8            ioctl.o namei.o super.o symlink.o hash.o resize.o ext3_jbd.o
  9
 10 ext3-$(CONFIG_EXT3_FS_XATTR)     += xattr.o xattr_user.o xattr_trusted.o
 11 ext3-$(CONFIG_EXT3_FS_POSIX_ACL) += acl.o
 12 ext3-$(CONFIG_EXT3_FS_SECURITY)  += xattr_security.o

我们应该先注意到7,8行,其定义了ext3变量(-y说明是多文件模块的定义,可以先忽略)。这里的定义变量类似于C语言中的宏定义,就是用ext3代替后面的.o文件列表。那么现在我们就可以知道与ext3模块最直接相关的就是后面这些文件对应的.c以及.h文件了,这些文件在源码相应的目录下都可以找到。那么ext3.o是否被编译取决于第的CONFIG_EXT3_FS,这个变量的值一般取y或n(甚至m)。它一般对应的是用户在配置界面时的输入。想要了解配置界面的菜单选项,就得看Kconfig文件。由于我们只关心EXT3_FS这个选项,因此我们相应的找到EXT3_FS这个选项的配置语句即可:

config EXT3_FS
	tristate "Ext3 journalling file system support"
	select JBD
	help
	  This is the journalling version of the Second extended file system
	  (often called ext3), the de facto standard Linux file system
	  (method to organize files on a storage device) for hard disks.
         #Other code was deleted

上述代码中,select说明只有JBD被配置,EXT3_FS这个配置项目才会在配置菜单上出现(事实上两者有更具体的依赖关系,可参考相关语法)。在配置菜单上会显示tristate后面的字符串,当用户选择配置此条目的情况下(有y,m和n三态选项),对应在Makefile文件中的CONFIG_EXT3_FS就对应为y。即obj-y的情况下,ext3.o菜会被编译。

现在将Makefile和Kconfig文件再串通起来想想,你应该会明白它们的作用。

一般来说,Makefile定义了根据该子目录下的源码文件构建目标文件的规则。像我们刚说的那个变量的定义以及根据CONFIG_EXT3_FS选项是否编译ext3.c文件。至于这些规则是否被执行,就取决于用户在配置菜单上是否选择配置这个选项,而这个配置菜单中配置选项就对应Kconfig文件。而且用户输入的配置结果会记录在.config文件当中。

通过上面的简单举例,我们可以先大致了解Makefile、Kconfig以及.config三者之间的关系以及作用。我们刚才分析的是内核中已经写好的代码的配置规则。对于我们上面所说的新增edsionteDriver驱动,应该如何添加?

具体添加过程请参见:在内核中新增驱动代码目录(2)。

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