Linux设备驱动基础Q&A

本文系学习总结并参考了大量网络资源,欢迎大牛指正!


1、 Linux中的用户模式和内核模式是什么含意?

答:

 32位的CPU,最大寻址范围为2^32- 1也就是4G的线性地址空间。Linux简化了分段机制,使得虚拟地址与线性地址总是一致的。Linux一般把这个4G的地址空间划分为两个部分:其中 0~3G为用户程序地址空间,虚地址0x00000000到0xBFFFFFFF,供各个进程使用;3G~4G为内核的地址空间,虚拟地址 0xC0000000到0xFFFFFFFF, 供内核使用。(注意,ARM架构不是3G/1G划分的,而是2G/2G划分。这里以3G/1G划分作讲解)。

     可以看出,每个进程都有自己的私有用户空间(0-3GB),这个空间对系统中的其他进程是不可见的。最高的1GB内核空间则由则由所有进程以及内核共享。可见,内核最多寻址1G的虚拟地址空间。

Linux机器上,CPU要么处于受信任的内核模式,要么处于受限制的用户模式。除了内核本身处于内核模式以外,所有的用户进程都运行在用户模式之中。

内核模式的代码可以无限制地访问所有处理器指令集以及全部内存和I/O空间。如果用户模式的进程要享有此特权,它必须通过系统调用向设备驱动程序或其他内核模式的代码发出请求。另外,用户模式的代码允许发生缺页,而内核模式的代码则不允许。

 

2、Linux设备驱动分为哪几种?

答:

Linux系统的设备分为字符设备(chardevice),块设备(block device)和网络设备(network device)三种。

字符设备是指存取时没有缓存的设备,它以字符(字节)为单位、逐个字符进行输入输出。典型的字符设备包括鼠标,键盘,串行口等。

块设备的读写都有缓存来支持,如磁盘那样以块或扇区为单位,成块进行输入输出。块设备必须能够随机存取(random access),字符设备则没有这个要求。块设备主要包括硬盘软盘设备,CD-ROM等。一个文件系统要安装进入操作系统必须在块设备上。
  网络设备在Linux里做专门的处理。Linux的网络系统主要是基于BSD Unix的socket机制。在系统和驱动程序之间定义有专门的数据结构(sk_buff)进行数据的传递。系统里支持对发送数据和接收数据的缓存,提供流量控制机制,提供对多协议的支持。

 

3、字符型驱动设备你是怎么创建设备文件的,就是/dev/下面的设备文件,供上层应用程序打开使用的?

答:Linux中万物都是文件,设备也是这样。要使用一个设备,首先要为它建立一个文件,然后通过下面的方法操作它。

int fd;

char buf[100];

fd =open(“/dev/TestChar”, O_RDONLY, 0);

if (-1 == fd)

    return ERROR;

 

read(fd, buf, 100);

close(fd);

 如何创建一个设备文件呢?作为一个标准的文件,可以用open创建(实际上调用的是create)。如果是一个设备,则要通过mknod创建。这是因为设备驱动的创建方式是非标准的。假如通过open或create创建文件,kernel默认会调用该文件所在目录属于的设备的驱动。

综上,mknod命令结合设备的主设备号和次设备号,可创建一个设备文件。这是静态方式。

Linux2.6内核以后,还可以通过用户空间的设备管理器udev, 它在/dev目录下动态地创建/移除设备节点.

在2.6之前,2.4之后,还有devfs方式。

devfs具有如下优点:

(1)可以通过程序在设备初始化时在/dev目录下创建设备文件,卸载设备时将它删除。

(2)设备驱动程序可以指定设备名、所有者和权限位,用户空间程序仍可以修改所有者和权限位。

 (3)不再需要为设备驱动程序分配主设备号以及处理次设备号,在程序中可以直接给register_chrdev()传递0主设备号以动态获得可用的主设备号,并在devfs_register()中指定次设备号。

 

可见,总共有三种方式。

 

4、写一个中断服务需要注意哪些?如果中断产生之后要做比较多的事情你是怎么做的?

答:

第一,写一个中断服务程序要注意快进快出,在中断服务程序里面尽量快速采集信息,包括硬件信息,然后推出中断,要做其它事情可以使用工作队列或者tasklet方式。也就是中断上半部和下半部。

第二,中断服务程序中不能有阻塞操作。中断函数中调用可能阻塞的函数如malloc,轻则引起系统任务调度打乱,失去实时性,重则系统崩溃或复位。

第三,中断服务程序注意返回值,要用操作系统定义的宏做为返回值,如IRQ_HANDLED,而不是自己定义的OK,FAIL之类的。

第四,避免在ISR中做浮点运算,在许多处理器/编译器中,浮点一般都是不可重入的。

 

 

5、谈谈您对platform总线\设备\驱动的理解? 设备驱动模型三个重要成员是?platform总线的匹配规则是?在具体应用上要不要先注册驱动再注册设备?有先后顺序没?

答:Linux 2.6的设备驱动模型中,关心总线、设备和驱动这3个实体,总线将设备和驱动绑定。在系统每注册一个设备的时候,会寻找与之匹配的驱动;相反的,在系统每注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。

 

一个现实的Linux设备和驱动通常都需要挂接在一种总线上,对于本身依附于PCI、USB、I2C、SPI等的设备而言,这自然不是问题,但是在嵌入式系统里面,SoC系统中集成的独立的外设控制器、挂接在SoC内存空间的外设等确不依附于此类总 线。基于这一背景,Linux发明了一种虚拟的总线,称为platform总线,相应的设备称为platform_device,而驱动成为 platform_driver。

注意,所谓的platform_device并不是与字符设备、块设备和网络设备并列的概念,而是Linux系统提供的一种附加手段,例如,在S3C6410处理器中,把内部集成的I2 C、RTC、SPI、LCD、看门狗等控制器都归纳为platform_device,而它们本身就是字符设备。

platform是内核的一个虚拟总线,它不像usb总线、PCI总线那样真实存在的,platform总线完全是虚拟出来的。我们先看看内核是如何定义这个虚拟总线的:

  struct bus_type platform_bus_type = {

  .name  = "platform",

  .dev_attrs = platform_dev_attrs,

  .match  = platform_match,

  .uevent  = platform_uevent,

  .pm  = &platform_dev_pm_ops,

  };

bus_type是内核的总线结构体,内核所有的总线都是由这个结构体定义的。我们只关注name和match这两个成员变量,其中name被赋值为"platform",毋庸置疑,这表示定义了一个名为“platform”的总 线。match方法在总线、驱动、设备这三者中扮演着十分重要的角色。

 在这里我简单说一下match方法何时被调用(理解这一点对于理解整个设备驱动模型起到一定的帮助)。当一个驱动挂接到该总线的时候,该总线的 match方法被调用,在这里,platform总线的match方法被赋值为platform_match,也就是说platform_match将被 调用,platform_match将会帮驱动找到匹配的设备。同样的,当一个设备挂接到该总线时,platform_match也会被调用,platform_match也会帮该设备找到匹配的驱动。用一句话来说就是,platform_match既帮驱动找对象,也帮设备找对象。当驱动 和对象匹配上了,platform_match可是会收两家的媒婆钱,黑心的很。

  那么对于platform总线来说,驱动和设备如何挂接到该总线上呢。platform总线分别提供了两个函数给驱动和设备使用。如下所示:

  int platform_driver_register(struct platform_driver *drv)

  int platform_device_register(struct platform_device*pdev)

  很显然platform_driver_register 是给驱动使用的,platform_device_register 是给设备用的。

 

6、  自旋锁和信号量在互斥使用时需要注意哪些?在中断服务程序里面的互斥是使用自旋锁还是信号量?还是两者都能用?为什么?

答:使用自旋锁的进程不能睡眠,使用信号量的进程可以睡眠。中断服务例程中的互斥使用的是自旋锁,原因是在中断处理例程中,硬中断是关闭的,这样会丢失可能到来的中断。

(1). Spinlock 只适用于短暂的等待,因为没有进程切换所以对于短暂等待他的效率会比较高。但是对于长时间等待,由于它的CPU占用是100% 等的越长越不合算。

   Semaphore的实现机制可以描述如下:CPU首先会检测信号量是否可获取,如果无法获取该信号量,那么就会导致一次上下文调度操作。从实现机制上来看,semaphore操作效率比较低,而spinlock的效率相对较高,但是这种效率的高低与应用相关,如果临界区操作的时间过长,那么采用 spinlock会浪费大量的CPU时间,还不如做几次上下文调度释放CPU资源呢。所以,spinlock的应用有一个原则,那就是临界区操作尽可能的短,让CPU不要自璇太多的时间,这一点从spinlock的实现代码上也可以看出一些端倪,Linux开发人员着这个地方花费了好多心计。

(2).Spinlock只能在多个CPU的系统上用,因为等待者占据了100%的CPU,只由另外一个CPU上的进程才能解锁。

(3).不允许睡眠的上下文需要采用spinlock,可以睡眠的上下文可以采用semaphore。在中断上下文中访问的竞争资源一定采用spinlock。

(4)、临界区操作较长的应用建议采用semaphore,临界区很短的操作建议采用spinlock。

(5)、 需要关中断的场合需要调用spinlock_irq或者spinlock_irqsave,不明确当前中断状态的地方需要调用 spinlock_irqsave,否则调用spinlock_irq。一个资源既在中断上下文中访问,又在用户上下文中访问,那么需要关中断,如果仅仅在用户上下文中被访问,那么无需关中断。

 

7、  原子操作你怎么理解?为了实现一个互斥,自己定义一个变量作为标记来作为一个资源只有一个使用者行不行?

答:原子操作指的是无法被打断的操作。

另外一个进程根本没有你自己定义的这个变量,自然不用检查就能进入临界区,因而起不到互斥的作用。充当互斥的变量应该是访问进程都可见的。

 

8、  insmod 一个驱动模块,会执行模块中的哪个函数?rmmod呢?这两个函数在设计上要注意哪些?遇到过卸载驱动出现异常没?是什么问题引起的?

答:

insmod调用init函数,rmmod调用exit函数。这两个函数在设计时要注意什么?卸载模块时曾出现卸载失败的情形,原因是存在进程正在使用模块,检查代码后发现产生了死锁的问题。

要注意在init函数中申请的资源在exit函数中要释放,包括物理内存,ioremap,定时器,工作队列等等。也就是一个模块注册进内核,退出内核时要清理所带来的影响,带走一切不留下一点痕迹。

 

9、  在驱动调试过程中遇到国oops没?你是怎么处理的?

答:When thekernel detects a problem, it prints an oops message and killsany offending process. Linux kernel engineers can use themessage to help debug the condition which created the oops and thus to fix the programming error which caused it.

A kernel oops often leads on to a kernelpanic once the system attempts to use resources which have been lost.

通常,当你面临一个oops时,首要问题就是查看故障的发生位置,它通常会与函数调用的堆栈信息分开列出。

Linux kernel oops panic 调试技巧

参考文章:http://blog.chinaunix.net/uid-291731-id-3142689.html

 

 

 

10、             ioctl和unlock_ioctl有什么区别?

答:今天调一个程序调了半天,发现应用程序的ioctl的cmd参数传送到驱动程序的ioctl发生改变。而根据《linux设备驱动》这个cmd应该是不变的。因为kernel2.6.36 中已经完全删除了structfile_operations 中的ioctl函数指针,取而代之的是unlocked_ioctl,所以我怀疑二者是不是兼容的。上网查了一些资料,很多文章只是泛泛谈了一下,说在应用程序中ioctl是兼容的,不必变化。而在驱动程序中这个指针函数变了之后最大的影响是参数中少了inode ,所以应用程序ioctl是兼容的,但驱动程序中我们的ioctl函数必须变化,否则就会发生cmd参数的变化:

原来的驱动程序

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

int globalmem_ioctl(struct inode* inode,struct file*filp, unsigned int cmd,unsigned long arg)

{

switch (cmd)

  {

   case:XXX:   ...

    ……

  }

}

改变后的

 

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

int globalmem_ioctl(struct file* filp, unsigned intcmd,unsigned long arg)//没有inode参数!

{

switch (cmd)

  {

   case:XXX:   ...

    ……

  }

}

 

11、             驱动中操作物理绝对地址为什么要先ioremap?

答:内存映射的I/O口,寄存器或者是硬件设备的RAM(如显存)一般占用F0000000以上的地址空间。在驱动程序中不能直接访问,要通过kernel函数ioremap获得重新映射以后的地址。

ioremap()是将外设卡上的内存映射到内存上,实现虚拟空间的管理.

因为内核没有办法直接访问物理内存地址,必须先通过ioremap获得对应的虚拟地址。

12、             kmalloc和vmalloc的区别?

答:(1)、kmalloc保证分配的内存在物理上是连续的,vmalloc保证的是在虚拟地址空间上的连续

(2)、kmalloc能分配的大小有限,vmalloc能分配的大小相对较大
(3)、vmalloc比kmalloc要慢
(4)、kmallloc使用的是slab内存分配机制,而vmalloc使用的是伙伴系统分配机制,这也是造成它们区别的根本所在

kmalloc()分配的是物理地址连续的内存(逻辑地址自然也是连续的),该函数是否允许休眠通过其参数flags来决定。

vmalloc()函数只确保页在虚拟地址空间内是连续的,它通过分配非连续的物理内存块,再“修正”页表,把内存映射到逻辑地址空间的连续区域。某些场合中,对内存区的请求不是很频繁,较高的内存访问时间也 可以接受,这是就可以分配一段线性连续,物理不连续的地址,带来的好处是一次可以分配较大块的内存。不能直接用于DMA因此,该函数可能睡眠,不能在中断上下文进行调用,也不能从其他不允许阻塞的情况下调用。

 

 

13、             module_init的级别?

14、             如何获得主设备号?

答:先来看看系统原有的设备号,查看“/proc/devices”文件的内容,一般是这样的:

Character devices:

 1 mem

 2 pty

 3 ttyp

180 usb

Block devices:

 2 fd

 8 sd

第二列是驱动名字,第一列就是主设备号。如何添加新的设备号呢?既然是在proc目录下,显然不能直接读写。在用户态我们是无能为力的,因为主设备号只能在kernel中操作。添加新的主设备号就是驱动初始化中首先要做的事情。

 

Kernel中添加新设备号的函数是register_chrdev_region(),它可以把一个设备号添加到内核中,但前提是我们知道哪个设备号没人用。不要以为“/proc/devices”中没有的设备号就是可用的,很多设备号是预留的。为了避免这一问题,最好的办法是让kernel为我们分配一个。对应函数是alloc_chrdev_region()。

完成上面的操作我们就可以在“/proc/devices”中查看对应的设备号了。

15、             如何添加驱动?

答:Kernel中使用cdev_add为某个设备号添加具体驱动。其原型如下:

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

dev是设备号,count是指可以创建几个这种类型的设备,也就是次设备号的范围。简单来讲主设备号表示驱动类型,次设备号表示设备实例。

P是该设备的数据维护结构。这个结构也挺复杂,因此kernel提供单独的函数cdev_init()对它进行初始化。P可以是我们自己申请,也可以是通过cdev_alloc()获得。好处是系统会在卸载模块时自动释放它,比较方便。

Cdev的结构比较复杂,但只有两项需要关心。一个是owner,一般填THIS_MODULE,另一个是ops,填真正的设备驱动,如open、read等。

至此,可以看到我们要做的就是实现ops的各个函数。通常只需要实现open、read、write、ioctl等几个函数。

还有一个涉及到实现细节的问题。

通常我们维护一个设备驱动需要很多数据,只靠一个统一的cdev结构体是不够的,因此真正实现时会创建一个自定义的结构体,如TestChar_dev,cdev结构体作为其中一个元素。

后见面我们会看到,open等函数只能得到cdev结构体的指针,我们需要通过kernel提供的container_of()来获取TestChar_dev的指针。

值得强调的几点:

  

   1)系统资源申请回收:用户程序资源申请忘了回收,最多导

 

致本身进程崩溃,而不会引起其他进程甚至系统崩溃,而驱动程序就会导致整个系

 

统的崩溃。

  

   2)堆栈特点:用户空间(0~3G)堆栈(栈)比较大,可以申请到足够的空间来存放

 

数据,而内核空间堆栈很小,一般申请超过 1k(512 bytes)就采用动态申请,记得

 

申请之后要记得释放。 

 

    3)同步问题:驱动程序时刻要注意并发问题、程序的可重入问题。驱动程序必须

 

能够在进程上下文、中断上下文中进行;数据结构必须小心设计,多个执行线程分

 

开,并且避免使用全局变量,以防对数据结构造成损坏。

 

   4)内核的并发问题:Linux 系统中, 在同一时间, 不止一个进程能够试图使用你

 

的驱动. 大部分设备能够中断处理器==>中断处理异步运行, 并且能在你的驱动试图

 

做其他事情的同一时间被调用.而且, 当然, Linux 可以在对称多处理器系(SMP)上

 

运行, 结果是你的驱动可能在多个CPU 上并发执行. 最后, 在 2.6, 内核代码已经

 

是可抢占的了; 这个变化使得即便是单处理器会有许多与多处理器系统同样的并发问题.

16、        在一个只有128M内存并且没有交换分区的机器上,说说下面两个程序的运行结果
1,
#define MEMSIZE 1024*1024
int count = 0;
void *p = NULL;
while(1) {
  p = (void *)malloc(MEMSIZE);
  if (!p) break;
  printf("Current allocation %d MB\n", ++count);
}

2,
while(1) {
  p = (void *)malloc(MEMSIZE);
  if (!p) break;
  memset(p, 1, MEMSIZE);
  printf("Current allocation %d MB\n", ++count);
}
答: 第一道程序分配内存但没有填充,编译器可能会把内存分配优化掉,程序死循环;第二道,程序分配内存并进行填充,系统会一直分配内存,直到内存不足,退出循环。

17、             LINUX下的Socket套接字和Windows下的WinSock有什么共同点?请从C/C++语言开发的角度描述,至少说出两点共同点。(10分,说得好的每点加5分,没有上限。精通SOCK编程者破格录用。)

答:a)都基于TCP/IP协议,都提供了面向连接的TCPSOCK和无连接的UDP SOCK。

b)都是一个sock结构体。

c)都是使用sock文件句柄进行访问。

d)都具有缓冲机制。

18、             驱动程序和应用程序的区别?

答:

19、             怎样申请大块内存?

答:Linux内核环境下,申请大块内存的成功率随着系统运行时间的增加而减少,虽然可以通过vmalloc系列调用申请物理不连续但虚拟地址连续的内存,但毕竟其使用效率不高且在32位系统上vmalloc的内存地址空间有限。所以,一般的建议是在系统启动阶段申请大块内存,但是其成功的概率也只是比较高而已,而不是100%如果程序真的比较在意这个申请的成功与否,只能退用启动内存BootMemory)。下面就是申请并导出启动内存的一段示例代码:

void* x_bootmem = NULL;
EXPORT_SYMBOL(x_bootmem);

unsigned long x_bootmem_size = 0;
EXPORT_SYMBOL(x_bootmem_size);

static int __init x_bootmem_setup(char *str)
{
        x_bootmem_size = memparse(str, &str);
        x_bootmem = alloc_bootmem(x_bootmem_size);
        printk("Reserved %lu bytes from %p for x\n", x_bootmem_size, x_bootmem);

        return 1;
}
__setup("x-bootmem=", x_bootmem_setup);

可见其应用还是比较简单的,不过利弊总是共生的,它不可避免也有其自身的限制:

内存申请代码只能连接进内核,不能在模块中使用。

被申请的内存不会被页分配器和slab分配器所使用和统计,也就是说它处于系统的可见内存之外,即使在将来的某个地方你释放了它。

一般用户只会申请一大块内存,如果需要在其上实现复杂的内存管理则需要自己实现。

在不允许内存分配失败的场合,通过启动内存预留内存空间将是我们唯一的选择。

void* alloc_bootmem(unsigned long size)

  可以在Linux内核引导过程中绕过伙伴系统来分配大块内存。使用方法是在Linux内核引导时,调用mem_init函数之前 alloc_bootmem函数申请指定大小的内存。如果需要在其他地方调用这块内存,可以将alloc_bootmem返回的内存首地址通过EXPORT_SYMBOL 出,然后就可以使用这块内存了。这种内存分配方式的缺点是,申请内存的代码必须在链接到内核中的代码里才能使用,因此必须重新编译内核,而且内存管理系统 看不到这部分内存,需要用户自行管理。测试结果表明,重新编译内核后重启,能够访问引导时分配的内存块

 

1、 用户进程间通信主要哪几种方式?

: 答:(1)管道(Pipe):管道可用于具有亲缘关系进程间的通信,允许一个进程和另一个与它有共同祖先的进程之间进行通信。

(2)命名管道(named pipe):命名管道克服了管道没有名字的限制,因此,除具有管道所具有的功能外,它还允许无亲缘关系进程间的通信。命名管道在文件系统中有对应的文件名。命名管道通过命令mkfifo或系统调用mkfifo来创建。

(3)信号(Signal):信号是比较复杂的通信方式,用于通知接受进程有某种事件发生,除了用于进程间通信外,进程还可以发送信号给进程本身;linux除了支持Unix早期信号语义函数sigal外,还支持语义符合Posix.1标准的信号函数sigaction(实际上,该函数是基于BSD的,BSD为了实现可靠信号机制,又能够统一对外接口,用sigaction函数重新实现了signal函数)。

(4)消息(Message)队列:消息队列是消息的链接表,包括Posix消息队列system V消息队列。有足够权限的进程可以向队列中添加消息,被赋予读权限的进程则可以读走队列中的消息。消息队列克服了信号承载信息量少,管道只能承载无格式字节流以及缓冲区大小受限等缺

(5)共享内存:使得多个进程可以访问同一块内存空间,是最快的可用IPC形式。是针对其他通信机制运行效率较低而设计的。往往与其它通信机制,如信号量结合使用,来达到进程间的同步及互斥。

(6)信号量(semaphore):主要作为进程间以及同一进程不同线程之间的同步手段。

(7)套接字(Socket):更为一般的进程间通信机制,可用于不同机器之间的进程间通信。起初是由Unix系统的BSD分支开发出来的,但现在一般可以移植到其它类Unix系统上:Linux和System V的变种都支持套接字。


你可能感兴趣的:(Linux设备驱动基础Q&A)