存档在 2010年9月

makefile再学习

2010年9月27日

前几篇文章中,我们一起分析了字符设备驱动。假如我们已经编写好了驱动代码,那么接下来该如何做?

我们首先要进行make,编译成功后会生成一个globalmem.ko文件。接下来要将这个内核模块插入到内核当中,然后还要利用mknod命令生成一个设备文件节点。接下来我们再利用测试程序,对我们写好的驱动程序进行测试。

那么以上的工作都可以通过下面这个Makefile文件完成,直接在shell终端输入make就可编译这个内核模块,输入make clean就可以清除一些中间文件,输入make install就可以将编译好的内核模块插入到内核当中。更重要的是,这个Makefile文件具有很好的移植性。本文通过分析下面给出的Makefile文件,与大家一起更深入的学习Makefile文件的相关语法以及一些shell编程。

TARGET= globalmem

ifneq ($(KERNELRELEASE),)
 obj-m := $(TARGET).o
 $(TARGET)-objs := module.o global_fops.o
else
 KERNEL := $(shell uname -r)
 KDIR ?= /lib/modules/$(KERNEL)/build
 PWD := $(shell pwd)

.PHONY all
.PHONY clean
.PHONY install
.PHONY remove

all:
	make -C $(KDIR) M=$(PWD) modules
clean:
	make -C $(KDIR) M=$(PWD) clean
install:
	@sudo ./$(TARGET).sh
remove:
	@sudo ./clean.sh
endif

这个Makefile文件(新Makefile)比这里的Makefile文件(旧Makefile)强大了很多。

1.条件语句

首先注意这个新的Makefile文件在逻辑结构上发生了很大的变化,采用了条件语句:ifneq-else-endif。这个条件语句是用来判断括号中逗号前后的两个变量是否不相等。ifneq之后为符合条件时所要执行的语句,相应的else之后为不符合条件时要执行的语句。上述Makefile文件中的ifneq ($(KERNELRELEASE),)是用来判断KERNELRELEASE变量是否为空,不为空则符合条件。

类似的还有下面的条件语句,只不过条件判断的类型不同。

ifeq-else-endif:如果两个变量相等,则满足条件。

下面两种条件语句中,条件判断处为变量名,是用来判断此变量是否被定义过。

ifdef-else-endif:如果变量被定义,满足条件。

ifndef-else-endif:如果变量未被定义,满足条件。

不过,上述两个条件语句所判断的变量定义没有递归性,比如下面例子:

path=
cur_path=$(path)
ifdef cur_path
right=yes
else
right=no
endif

这个例子中最终执行的是right=yes。虽然path为空,但是cur_path=$(path)却被认为是定义了cur_path变量。正如上面所说定义没有递归行。

2.变量赋值

在Makefile文件中定义一个变量的格式为:变量名 赋值符 变量值

赋值符通常有以下四种类型:=,:=,?=,+=。对于赋值符=与我们平日里使用的等号差不多,但是这里我们需要清除一个概念,那就是递归展开变量。为了更清除的说明上面的概念,请看下面的例子:

first=$(second)
second=$(third)
third=yes
all:
       echo $(first)

很显然结果为yes。当执行make时,first首先展开为second,接着second又展开成为third,再后来引用third的值即yes。可以看到first是递归展开而得到最后的yes值的。这便是我们刚才所谓的递归展开变量。

而与上述变量赋值符号不同的是,:=赋值符号是立即展开变量的,同样的例子,只不过这次我们使用:=赋值符:

first:=$(second)
second:=$(third)
third:=yes
all:
       echo $(first)

此时first为空。这是因为在定义first变量时就立即展开了second,因为second此时未定义。即便此句之后为second变量赋了值,但first的值为空。

另外两个赋值符号比较容易理解。首先+=赋值符是在变量原有值的基础上再增加新的值,而不是覆盖原有变量值。而?=赋值符首先会判断变量实现是否已经被赋值,只有之前未被赋值的变量此刻才能被赋值。

OK,了解了赋值符号的含义,那么再次看上述的Makefile文件,就会清晰很多。

3.伪目标

正如上述所言,直接在shell终端输入make就会执行目标all后的命令,这并不是all目标具有什么默认的效果。只不过在Makefile文件中,第一个目标总被认为是最终目标。因此可以想象到,当你交换一下all和clean的位置,直接执行make时会自动执行clean后面的命令。并且不一定总对第一个目标起名为all,你可以使用你喜欢的目标名(也许all是一种无声的约定 e43 )。

通常在clean这样的目标后都没有依赖文件,因为我们的目的是想让make执行这些目标后的命令。但是当Makefile文件所在目录下有一个名为clean的文件时,此时make clean就会被认为是生成clean目标文件。而clean后是没有任何依赖文件的,所以每次make clean后clean目标文件都会被认为是最新,而不去执行下面的命令,这虽然符合语法规则,但并不能达到我们使用clean的目的。

因此我们必须将clean这种目标定义成伪目标。定义方法为:.PHONY:all。这样不管该目录下是否有同名的文件都会执行clean后的命令。现在你应该明被为什么MAKEFILE文件中有这么多以.PHONY开头的目标文件了吧。

4.为什么要用makefile

内核模块化简单实用,但是编译却成了问题:有时候我们只是改动了某个文件的一小部分就不得不编译整个内核,这是个很可怕的事情。但是GNU make引入后,这个问题就迎刃而解了:make只会编译已被改动代码的文件,而不是将所有文件都编译。但是make具体如何对源文件进行编译,怎么编译?这个时候就需要makefile文件了。在之前的文章当中,我们对Makefile文件下过“编译规则”这样的定义,下面通过分析上面的Makefile文件,我们具体感受一下这个“编译规则”。

整个Makefile文件根据KERNELRELEASE的值来划分不同的编译规则(方式),这里的KERNELRELEASE只会在内核源码目录下显示当前内核的版本号。

一般情况下,我们编写的内核模块源文件所在的目录并非位于内核源码根目录(或其子目录)下。那么此时就不符合ifueq条件,即执行else语句下的编译规则。这种情况下,当我们输入make后,就会执行make -C $(KDIR) M=$(PWD) modules这条命令。注意这条命令后的modules,它表示将会编译所有在配置菜单中被选作模块编译的那些内容(也就是赋值给obj-m的那些目标)。接下来由于-C $(KDIR)参数的原因,make会转向内核源码根目录下去执行。根据M后的目录,编译我们写的内核模块源码,生成.o文件。接着联合一些中间文件生成.ko文件。这便是make生成整个内核目标文件的过程。这个过程可以在make之后在终端产生的一系列描述文字得到。

上面这种情况会将我们编写的内核模块源码编译成内核模块目标文件,接下来就是我们熟悉的内核模块插入了。不过当我们所写的内核模块文件处于内核源码目录下时,KERNELRELEASE就会非空(此时为版本号),那么此时就满足ifueq条件了。什么时候我们编写的内核模块源码会处在内核源码目录下?此时的内核编译是那种方式?

在前面的文章中我们假设已经写好了驱动代码,然后在Kconfig文件中为这个驱动编写配置选项。在配置菜单中有了此驱动的相关配置选项后,接下来用户可以选择是否会将此驱动源码一起编译进内核。那么此时Makefile文件的作用就是将此内核模块源码编译进内核(obj-m := $(TARGET).o)。不过注意,只是通知内核下次编译“带上我”,并没有实际编译。

现在应该明白整个Makefile文件的逻辑结构了吧?

Git快速入门(Q&A)

2010年9月26日

如果你对于git完全不知,那么不用着急,我们一起来从零开始学习。

1.难道git就是傻瓜的意思吗?

git是一个版本控制系统,也叫做傻瓜内容跟踪器。按照我目前的理解是:一个项目团队共同完一个项目,而且团队里的每个人都深处异地,那么如何将每个人负责的代码融合在一起?而且如何清晰的展现不同的人在不同的地方在不同的时间里对代码做过修改?这时候,git派上用场了。

事实上,Linux内核正是使用git进行管理的,而git的创始人也正是linux之父:Linus Torvalds。

2.我如何才能获得git?

想使用git,肯定必须安装git了。如果你在ubuntu下,使用下面的命令可以快速安装:sudo apt-get install git-core。安装完成后,可使用git –version查看git的版本号。

3.我在安装完git后该做些什么?

首先你得向git介绍你自己,告诉它你的姓名和邮箱吧。

git config --global user.name "edsionte"
git config --global user.email "edsionte@gmail.com"

4.我依然可以通过男人手册获得git的帮助吗?

当然可以,男人手册是强大的。比如我们可以通过下面的命令获得git中commit命令的相关帮助。

man git-commit

5.我如何开启一个新的管理项目?

比如你要管理的项目目录为mywork,该目录下有一个文件hello.c.进入到该目录,通过git init命令就可以创建一个版本库。hello.c的内容如下:

edsionte@edsionte-desktop:~/mywork$ cat hello.c
#include “stdio.h”

int main()
{
	printf("hello,git!\n");
	printf("I can use git!\n");
	return 0;
}

6.如何将我的文件加入到版本库?

比如我要将hello.c文件加入到我的版本库,那么我可以这样:

git add hello.c
git commit

第一条命令只是将hello.c文件增加到版本库的索引当中,而第二条命令才真正的将hello.c文件的内容提交到版本库中,并且当你执行第二条命令的时候会在终端出现新的界面,在这个界面中你可以输入与该文件相关的开发日志等,比如我们输入“The version is: 0.0.1”。注意,要在这个界面下进行相关操作必须以ctrl组合相应的字母。

事实上第二条命令我们可以这样处理:

git commit -m "The version is: 0.0.1"

即直接在命令中输入开发日志信息。

每当你改进原有文件的代码时,你都可以按照上面的指令将更新后的内容添加到版本库中。不过,当我们只是更新了原有文件的内容而不是新增文件或者目录时,就可以用下面的命令代替以上工作:

git commit -a

7.在我对某个(些)文件进行了修改之后,我如何查看新旧文件的差异?

这是一个好问题。不过在回答这个问题之前,请先回顾如何将一个文件更新到版本库:先修改文件,再add,再commit。OK,那么接下来的几个指令的使用时间就很清晰了。

在add之前,可以使用git diff命令来查看文件被更新前后的差异。

在coomit之前,可以使用git diff –cached命令来查看文件被更新前后的差异。

以上命令可以查看文件内容的具体差异。那么在coomit之前,如何查看那些文件被修改过?git status即可做到。

8.既然我记录了开发日志,那么我如何查看它?

git log

9.什么是分支?分支有何用?

以上我们的操作均是在主分支master中进行的。有时候我们需要创建一个个人分支,以避免对主分支的影响;有时候我们需要一个临时性的分支去完成一些实验性的工作;当出现以上情况时,我们就需要创建一个分支。

10.如何创建一个分支?对于这个分支我能做些什么?

如果我想创建temp分支,那么可以通过git branch temp来完成。 通过git branch命令可以查看所以分支,比如:

edsionte@edsionte-desktop:~/mywork$ git branch
* master
  slave
  temp

而且*标志表示当前你所在的分支。如果你想进入temp分支,那么通过git checkout temp命令完成。接下来你再temp分支中创建新文件或者修改原有文件都不会影响主分支。

11.到目前为止,我认为temp分支完全正确,那么如何将它合并到主分支?

在这之前你要确认一下temp分支和master分支中现在存在那些文件。在master分支中有一个hello.c文件。在temp分支中当然也存在hello.c文件,不过被修改成如下内容:

edsionte@edsionte-desktop:~/mywork$ cat hello.c
#include “stdio.h”

int main()
{
	printf("hello,git!\n");
	printf("I can use git!\n");
	printf("I love git!\n");
	return 0;
}

此外,在temp分支中还新建了hi.c文件。使用git commit -a命令向版本库中提交了hello.c文件后,还必须add和commit来提交新建的hi.c文件,完成后切换到master分支。接着使用git merge temp命令就可将temp分支合并到主分支了。查看hello.c文件,是不是已经更新了?而且还新增了hi.c文件。

edsionte@edsionte-desktop:~/mywork$ git merge temp
Merge made by recursive.
 hello.c |    1 +
 1 files changed, 1 insertions(+), 0 deletions(-)

12.虽然你说的很轻松,可是我在试验的时候还是出现了诸如下面的错误,这是怎么回事?

edsionte@edsionte-desktop:~/mywork$ git merge temp
Auto-merging hello.c
CONFLICT (content): Merge conflict in hello.c
Automatic merge failed; fix conflicts and then commit the result.

如果出现这样的情况,我们可以看一下master分支下的hello.c文件:

edsionte@edsionte-desktop:~/mywork$ cat hello.c
#include “stdio.h”

int main()
{
	printf("hello,git!\n");
<<<<<<< HEAD ======= 	printf("I can use git!\n"); >>>>>>> temp
	return 0;
}

“====”符号是出现混淆内容的分割线。“<<<<<<< HEAD”与”====”之间的语句段为空,而”====”与“>>>>>>> temp”之间的语句段为printf(“I can use git!\n”);。出现这种情况是因为git不能辨别这两段语句的前后关系,这时候需要我们人为来调整分支文件的内容再合并。

13.接下来我还能做些什么?

以上的内容只是git的简单入门教程,更多的内容可以参考git男人手册

字符设备驱动分析(2)

2010年9月24日

前文中,我们按照一般内核模块的结构分析了globalmem_init函数和globalmem_exit函数。通过上述两个函数可以完成字符驱动的加载和卸载。那么本文将进一步分析字符设备驱动的实现。

linux2.6内核中使用cdev结构体来表述一个字符设备驱动,但是一般我们并不直接使用cdev结构体,而是将与该设备相关的信息与cdev街头体结合爱一起,定义一个新的结构体,比如

struct globalmem_dev
{    struct cdev cdev;
      unsigned char mem[GLOBALMEM_SIZE];
};
struct cdev
{
	struct kobject kobj;//内嵌kobject对象
	struct module *owner;//指向实现驱动程序的模块的指针,通常为THIS_MODULE
	const struct file_operations *ops;//指向此设备驱动程序文件操作结构体的指针
	struct list_head list;//指向字符设备文件对应的索引节点链表的头
	dev_t dev;//设备号
	unsigned int count;//给该设备驱动程序分配的设备号范围的大小
};

就像前文所说的,设备号都是分配一个范围(count的大小),因此可能有很多个设备文件主设备号相同并且对应于同一个设备驱动。list所指向的链表就是由当前该设备驱动对应的设备文件索引节点组成。

我们现在回到globalmem_setup_cdev函数,它的主要作用就是申请并初始化一个cdev结构体,并且将通过cdev_add函数向系统内添加一个cdev,完成字符设备的注册。通常我们将cdev_add函数安排在字符设备驱动模块的加载函数中,而对应的将cdev_del函数放在字符设备驱动的卸载函数中。

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

cdev_init(&dev->cdev, &globalmem_fops);
dev->cdev.owner = THIS_MODULE;
dev->cdev.ops = &globalmem_fops;
err = cdev_add(&dev->cdev, devno, 1);
if (err)
printk(KERN_NOTICE "Error %d adding cdev%d", err, index);
}

除此之外,globalmem_setup_cdev函数还会将cdev结构体中的struct file_operation类型的指针ops实例化。globalmem_fops全局变量是文件操作表,这个结构中含有许多文件操作函数类型的指针。当我们实现某些文件操作函数时,就可以将这些函数名赋值给这个结构中的相应变量。比如我们在稍候会实现globalmem_open函数,将其赋值给globalmem.open,那么当用户使用open系统调用对字符设备文件进行打开操作时,内核就会自动调用适合该设备文件的打开函数,也就是globalmem_open函数。

正如你所知的那样,Linux下一些皆为文件,当然设备也不例外。对于一个设备文件来说,用户通过VFS可以使用统一的系统调用接口对各种设备(文件)进行相关操作,比如open,read,write等等,用户可以不去考虑当前设备具体如何去操作。而在VFS层下——位于操作系统中的设备驱动就会对于每种设备去实现相应的操作函数。对于每类设备所实现的操作如何在用户层统一的表现出来,这就需要struct file_operations结构体。此结构体中包含大量的函数指针,这些函数指针便是用户层上统一的系统调用函数名,将设备驱动中实现的具体操作函数赋值给这些函数指针后,用户就可以使用统一的系统调用函数了。

接下来我们来看具体的文件操作函数是如何实现的。
文件打开函数将设备结构体指针赋值给私有数据,这个私有数据会在稍候的read以及write中被用到,而不是直接的使用globalmem_devp。

/*文件打开函数*/
int globalmem_open(struct inode *inode, struct file *filp)
{
  /*将设备结构体指针赋值给文件私有数据指针*/
  filp->private_data = globalmem_devp;
  return 0;
}

在读函数中,首先将私有数据赋值给一个设备结构体指针。然后,判断要读的长度是否合法。接着利用copy_to_user函数内核空间的数据(dev->mem)拷贝到用户空间。关于这个copy_to_user函数的详细拷贝过程,我们也可以对其进行代码分析。如果拷贝成功,那么修改相应的指针即可完毕读操作。

/*读函数*/
static ssize_t globalmem_read(struct file *filp, char __user *buf, size_t size,
  loff_t *ppos)
{
  unsigned long p =  *ppos;
  unsigned int count = size;
  int ret = 0;
  struct globalmem_dev *dev = filp->private_data; /*获得设备结构体指针*/

  /*分析获取有效的写长度*/
  if (p >= GLOBALMEM_SIZE)
    return count ?  - ENXIO: 0;
  if (count > GLOBALMEM_SIZE - p)
    count = GLOBALMEM_SIZE - p;

  /*从内核空间向用户空间写数据*/
  if (copy_to_user(buf, (void*)(dev->mem + p), count))
  {
    ret =  - EFAULT;
  }
  else
  {
    *ppos += count;
    ret = count;

    printk(KERN_INFO "read %d bytes(s) from %d\n", count, p);
  }

  return ret;
}

写函数与读函数的过程大体一直,不同的是使用了copy_from_user函数。这里不再详解。

接下来我们就可以使用一个简单测试程序来对我们所实现的字符设备驱动进行测试了。

register_chrdev_region函数源码分析

2010年9月21日

如何找到一个有效的切入点去深入分析内核源码,这是一个令人深思的问题。本文以前文中未详细说明的函数为切入点,深入分析char_dev.c文件的代码。如果你已经拥有了C语言基础和一些数据结构基础,那么还等什么?Let’s go!

在《字符设备驱动分析》一文中,我们说到register_chrdev_region函数的功能是在已知起始设备号的情况下去申请一组连续的设备号。不过大部分驱动书籍都没有去深入说明此函数,可能是因为这个函数内部封装了__register_chrdev_region(unsigned int major, unsigned int baseminor, int minorct, const char *name)函数的原因。不过我们不用苦恼,这正好促使我们去分析这个函数。

194int register_chrdev_region(dev_t from, unsigned count, const char *name)
 195{
 196        struct char_device_struct *cd;
 197        dev_t to = from + count;
 198        dev_t n, next;
 199
 200        for (n = from; n <\ to; n = next) {
 201                next = MKDEV(MAJOR(n)+1, 0);
 202                if (next >\ to)
 203                        next = to;
 204                cd = __register_chrdev_region(MAJOR(n), MINOR(n),
 205                               next - n, name);
 206                if (IS_ERR(cd))
 207                        goto fail;
 208        }
 209        return 0;
 210fail:
 211        to = n;
 212        for (n = from; n <\ to; n = next) {
 213                next = MKDEV(MAJOR(n)+1, 0);
 214                kfree(__unregister_chrdev_region(MAJOR(n), MINOR(n), next - n));
 215        }
 216        return PTR_ERR(cd);
 217}

首先值得我们注意的是,这个函数每次分配的是一组设备编号。其中from参数是这组连续设备号的起始设备号,count是这组设备号的大小(也是次设备号的个数),name参数处理本组设备的驱动名称。另外,当次设备号数目过多(count过多)的时候,次设备号可能会溢出到下一个主设备。因此我们在for语句中可以看到,首先得到下一个主设备号(其实也是一个设备号,只不过此时的次设备号为0)并存储于next中。然后判断在from的基础上再追加count个设备是否已经溢出到下一个主设备号。如果没有溢出(next小于to),那么整个for语句就只执行个一次__register_chrdev_region函数;否则当设备号溢出时,会把当前溢出的设备号范围划分为几个小范围,分别调用__register_chrdev_region函数。

如果在某个小范围调用__register_chrdev_region时出现了失败,那么会将此前分配的设备号都释放。

其实register_chrdev_region函数还没有完全说清除设备号分配的具体过程,因为具体某个小范围的设备号是由__register_chrdev_region函数来完成的。可能你已经注意到在register_chrdev_region函数源码中出现了struct char_device_struct结构,我们首先来看这个结构体:

  50static struct char_device_struct {
  51        struct char_device_struct *next;
  52        unsigned int major;
  53        unsigned int baseminor;
  54        int minorct;
  55        char name[64];
  56        struct cdev *cdev;              /* will die */
  57} *chrdevs[CHRDEV_MAJOR_HASH_SIZE];

在register_chrdev_region函数中,在每个字符设备号的小范围上调用__register_chrdev_region函数,都会返回一个struct char_device_struct类型的指针。因此我们可以得知,struct char_device_struct类型对应的并不是每一个字符设备,而是具有连续设备号的一组字符设备。从这个结构体内部的字段也可以看出,这组连续的设备号的主设备号为major,次设备号起始为baseminor,次设备号范围为minorct,这组设备号对应的设备驱动名称为name,cdev为指向这个字符设备驱动的指针。

这里要特别说明的是,内核中所有已分配的字符设备编号都记录在一个名为chrdevs散列表里。该散列表中的每一个元素是一个 char_device_struct结构,这个散列表的大小为255(CHRDEV_MAJOR_HASH_SIZE),这是因为系统屏蔽了12位主设备号的前四位。既然说到散列表,那么肯定会出现冲突现象,因此next字段就是冲突链表中的下一个元素的指针。

接下来我们详细来析__register_chrdev_region函数。首先为cd变量分配内存并用零来填充(这就是用kzalloc而不是kmalloc的原因)。接着通过P操作使得后续要执行的语句均处于临界区。

  92static struct char_device_struct *
  93__register_chrdev_region(unsigned int major, unsigned int baseminor,
  94                           int minorct, const char *name)
  95{
  96        struct char_device_struct *cd, **cp;
  97        int ret = 0;
  98        int i;
  99
 100        cd = kzalloc(sizeof(struct char_device_struct), GFP_KERNEL);
 101        if (cd == NULL)
 102                return ERR_PTR(-ENOMEM);
 103
 104        mutex_lock(&chrdevs_lock);

如果major为0,也就是未指定一个具体的主设备号,需要动态分配。那么接下来的if语句就在整个散列表中为这组设备寻找合适的位置,即从散列表的末尾开始寻找chrdevs[i]为空的情况。若找到后,那么i不仅代表这组设备的主设备号,也代表其在散列表中的关键字。当然,如果主设备号实现已指定,那么可不去理会这部分代码。

 105
 106        /* temporary */
 107        if (major == 0) {
 108                for (i = ARRAY_SIZE(chrdevs)-1; i > 0; i--) {
 109                        if (chrdevs[i] == NULL)
 110                                break;
 111                }
 112
 113                if (i == 0) {
 114                        ret = -EBUSY;
 115                        goto out;
 116                }
 117                major = i;
 118                ret = major;
 119        }

接着对将参数中的值依次赋给cd变量的对应字段。当主设备号非零,即事先已知的话,那么还要通过major_to_index函数对其进行除模255运算,因此整个散列表关键字的范围是0~254。

 120
 121        cd->major = major;
 122        cd->baseminor = baseminor;
 123        cd->minorct = minorct;
 124        strlcpy(cd->name, name, sizeof(cd->name));
 125
 126        i = major_to_index(major);

至此,我们通过上面的代码会得到一个有效的主设备号(如果可以继续执行下面代码的话),那么接下来还不能继续分配。正如你所知的那样,散列表中的冲突是在所难免的。因此我们得到major的值后,我们要去遍历冲突链表,为当前我们所述的char_device_struct类型的变量cd去寻找正确的位置。更重要的是,我们要检查当前的次设备号范围,即baseminor~baseminor+minorct,是否和之前的已分配的次设备号(前提是major相同)范围有重叠。

下面的for循环就是在冲突链表中查找何时的位置,当出现以下三种情况时,for语句会停止。

(1)如果冲突表中正被遍历的结点的主设备号(*(cp)->major)大于我们所分配的主设备号(major),那么就可以跳出for语句,不再继续查找。此时应该说设备号分配成功了,那么cd结点只需等待被插到冲突链表当中(*cp节点之前)。

(2)如果(*cp)结点和cd结点的主设备号相同,但是前者的次设备号起点比cd结点的大,那么跳出for语句,等待下一步的范围重叠的检测。

(3)如果(*cp)结点和cd结点的主设备号相同,但是cd结点的次设备号起点小于(*cp)结点的次设备号的终点,那么会跳出for语句。此时很可能两个范围的次设备号发生了重叠。

由上面的分析可以看出,冲突表中是按照设备号递增的顺序排列的。

 127
 128        for (cp = &chrdevs[i]; *cp; cp = &(*cp)->next)
 129                if ((*cp)->major > major ||
 130                    ((*cp)->major == major &&
 131                     (((*cp)->baseminor >= baseminor) ||
 132                      ((*cp)->baseminor + (*cp)->minorct > baseminor))))
 133                        break;

接下来检测当主设备号相同时,次设备范围是否发生了重叠。首先依次计算出新老次设备号的范围,接着进行范围判断。第一个判断语句是检测新范围的终点是否在老范围的之间;第二个判断语句是检测新范围的起点是否在老范围之间。

 134
 135        /* Check for overlapping minor ranges.  */
 136        if (*cp && (*cp)->major == major) {
 137                int old_min = (*cp)->baseminor;
 138                int old_max = (*cp)->baseminor + (*cp)->minorct - 1;
 139                int new_min = baseminor;
 140                int new_max = baseminor + minorct - 1;
 141
 142                /* New driver overlaps from the left.  */
 143                if (new_max >= old_min && new_max <= old_max) {
 144                        ret = -EBUSY;
 145                        goto out;
 146                }
 147
 148                /* New driver overlaps from the right.  */
 149                if (new_min <= old_max && new_min >= old_min) {
 150                        ret = -EBUSY;
 151                        goto out;
 152                }
 153        }

当一切都正常后,就将char_device_struct描述符插入到中途链表中。至此,一次小范围的设备号分配成功。并且此时离开临界区,进行V操作。如果上述过程中有任何失败,则会跳转到out处,返回错误信息。

 154
 155        cd->next = *cp;
 156        *cp = cd;
 157        mutex_unlock(&chrdevs_lock);
 158        return cd;
 159out:
 160        mutex_unlock(&chrdevs_lock);
 161        kfree(cd);
 162        return ERR_PTR(ret);
 163}

至此,我们已经分析完了字符设备号分配函数。

软链接和硬链接

2010年9月20日

硬链接类似与一个指向文件的指针(但是与文件描述符不同),比如我们通过下面命令:

edsionte@edsionte-laptop:~$ touch file1
edsionte@edsionte-laptop:~$ ln file1 file1hdlink
edsionte@edsionte-laptop:~$ ls -l file1 file1hdlink
-rw-r--r-- 2 edsionte edsionte 0 2010-09-20 22:56 file1
-rw-r--r-- 2 edsionte edsionte 0 2010-09-20 22:56 file1hdlink
edsionte@edsionte-laptop:~$ rm file1
edsionte@edsionte-laptop:~$ ls -l file1hdlink
-rw-r--r-- 1 edsionte edsionte 0 2010-09-20 22:56 file1hdlink

通过ln命令我们为file1文件创建了一个硬链接file1hdlink。通过ls -li 命令我们也可以发现,这两个文件的索引节点,属性以及大小均均是相同的,因此我们可以得出这样的结论:file1和file1hdlink同时指向一个文件(类似指针),它们只是同一个文件的两个不同名字而已。此时也就没有file1是源文件,而file1hdlink是硬链接这样的概念了,两者的地位相同。当我们删除其中一个文件时,就会发现连接数减少了一个。当某个文件的链接数为0时,这个文件便会被删除。

软链接也叫符号链接(symbol link),它相当于windows下快捷方式。与硬链接不同的是,软链接本身就是一类文件(链接文件),因此软链接本身的索引结点和其链接的文件的索引结点是不同的。比如通过下面的命令:

edsionte@edsionte-laptop:~$ ln -s file2 file2symlink
edsionte@edsionte-laptop:~$ ls -li file2 file2symlink
51217 -rw-r--r-- 1 edsionte edsionte 7 2010-09-20 23:18 file2
51214 lrwxrwxrwx 1 edsionte edsionte 5 2010-09-20 23:17 file2symlink -> file2
edsionte@edsionte-laptop:~$ cat file2symlink
hello!
edsionte@edsionte-laptop:~$ rm file2
edsionte@edsionte-laptop:~$ cat file2symlink
cat: file2symlink: 没有那个文件或目录

这里我们也可以发现file2的链接数为1。这里的两个文件并不是平等的关系,从文件属性也可以发现file2是普通文件,file2symlink是一个链接文件。每次系统访问软连接文件时,就会自动去访问它所链接的那个源文件。当源文件本身被删除后,这个链接文件也就失效了,尽管它还存在,但是已经不能访问到源文件了。

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