面试-驱动开发

1 Linux相关

1.1 Linux中用户模式和内核模式是什么含意?

​ linux中内核本身处于内核模式,应用程序处于用户模式。

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

1.2 linux用户进程间通信主要有哪几种方式?

1.2.1 管道(Pipe)

管道可用于具有亲缘bai关系进程间的通信,允许一个进程和另一个与它du有共zhi同祖先的进程之间进行通信。

1.2.2 命名管道(named pipe)

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

1.2.3 信号(Signal)

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

1.2.4 消息(Message)队列

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

1.2.5 共享内存

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

1.2.6 信号量(semaphore)

主要作为进程间以及同一进程不同线程之间的同步手段

1.2.7 套接字(socket)

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

1.3 内核空间与用户空间的通信方式

1.3.1 系统调用

用户空间需要访问内核空间,就需要借助系统调用来实现。系统调用是用户空间访问内核空间的唯一方式,保证了所有的资源访问都是在内核的控制下进行的,避免了用户程序对系统资源的越权访问,提升了系统安全性和稳定性。 进程A和进程B的用户空间可以通过如下系统函数和内核空间进行交互。

  • copy_from_user:将用户空间的数据拷贝到内核空间。
  • copy_to_user:将内核空间的数据拷贝到用户空间。

1.3.2 内存映射

由于应用程序不能直接操作设备硬件地址,所以操作系统提供了一种机制:内存映射,把设备地址映射到进程虚拟内存区。在Linux中通过系统调用函数mmap来实现内存映射。将用户空间的一块内存区域映射到内核空间。映射关系建立后,用户对这块内存区域的修改可以直接反应到内核空间,反之亦然。内存映射能减少数据拷贝次数,实现用户空间和内核空间的高效互动。

1.3.3 文件系统

用于内核空间向用户控件输出信息。

  • procfs
  • sysfs

Netlink是基于socket的通信机制,由于socket本身的双共性、突发性、不阻塞特点,因此能够很好的满足内核与用户空间小量数据的及时交互。相比于其他的用户态和内核态IPC机制,netlink有几个好处:

  • 使用自定义一种协议完成数据交换,不需要添加一个文件等
  • 可以支持多点传送
  • 支持内核先发起会话
  • 异步通信,支持缓存机制

1.4 linux内存如何划分及如何使用?虚拟地址及物理地址的概念及转换,高端内存的概念?

参考链接

1.5 linux中系统调用过程?如应用程序中read()在linux中执行过程(从用户空间到内核空间)?

参考链接

参考链接

1.6 linux调度原理

1.7 linux RCU原理

1.8 linux编译时用到的参数含义

1.9 linux内核的启动过程(源代码级)

2 基础相关

2.1 怎么用C先嵌入式系统的死循环的?

2.2 copy_to_user() 和copy_from_user()主要用于实现什么功能?一般用于file_operations结构的哪些函数里面?

2.3 ioctl和unlock_ioctl有什么区别?

2.4 kmalloc和vmalloc的区别

2.5 内核函数mmap的实现原理,机制?

2.6 怎样申请大块内核内存?

2.7 framebuffer机制

2.8 内核配置编译及Makefile

2.9 列举最少3种你所知道的嵌入式的体系结构,并请说明什么时ARM体系结构?

2.10 回调函数及其使用场景

2.10.1 回调函数基本定义

2.10.1.1 什么是函数指针

函数指针是一个指向特定函数的指针。函数的类型由其参数及返回类型共同决定,与函数具体名称无关。示例代码如下:

1
int testFun1(int param1,long param2,float param3); //普通函数定义

该函数的类型为int(int,long,float),该类型的函数指针可以定义为如下:

1
int (*pTf)(int,long,float);

2.10.1.2 什么是回调函数

回调函数就是用来给别人调用的函数,函数的编写者只负责实现函数,不用去主动执行函数。

2.10.2 回调函数基本形式

回调函数是通过函数指针来实现。具体的示例示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include "stdafx.h"
#include <iostream>
using namespace std;

typedef int(*pFun)(int); //定义一个函数指针类型

//函数功能:回调函数测试函数
//参数: pFun pCallback[IN] -- 函数指针,用于指针回调函数
//返回值: 无
void Caller(pFun pCallback)
{
cout << "准备执行回调函数..." << endl;
int ret = pCallback(1);
cout << "函数处理结果:" << ret << endl;
}

//函数功能:真正的回调函数
//参数: int iParam[IN] -- 输入参数
//返回值: int -- 执行结果
int realCallbackFun(int iParam)
{
cout << "进入回调函数..." << endl;
return iParam + 1;
}

int main(int argc, char* argv[])
{
Caller(realCallbackFun);

getchar();
return 0;
}

2.10.3 回调函数的应用场景

2.10.3.1 事件驱动机制

我们假定有两个类,类A与类B。该模式的工作机制如下:

  • 类A提供一个回调函数F,该回调函数执行根据不同的参数,执行不同的动作;
  • 类A在初始化类B时,传入回调函数F的函数指针pF;
  • 类B根据需要在不同的情况下调用回调函数指针pF,这样就实现了类B来驱动类A,类A来响应类B的动作。

2.10.3.2 通信协议的“推”模式

在我们实际工作中,经常会遇到数据通信的问题。总体来说,两个对象要实现数据通信,有以下两种方式:

  • “拉”模式

    在该模式下,假定对象A要从对象B中获取实时数据信息,“拉”模式的工作机制如下:

    • 对象A开启一个线程,该线程执行一个循环,每隔一定时间间隔,向对象B发出数据请求;
    • 对象B一旦有新的信息,就利用对象A的数据请求,将信息发送给对象A。

    注意:该模式的主要问题是需要维护一个循环线程。时间间隔太长会导致,通信的实时性下降;时间间隔太短,会导致CPU浪费太多。

  • “推”模式

    在该模式下,假定对象A要从对象B中获取实时数据信息,“推”模式的工作机制如下:

    • 对象A在调用对象B时,向其传递一个回调函数;
    • 对象B一旦有新的信息,就调用对象A传递过来的函数指针,将最新的信息发送给对象A

    注意:该模式完美解决了“拉“模式产生的问题,不但保证了数据传输的实时性,而且降低了无用的CPU消耗。一般的通信协议,建议采用”推“模式。

3 驱动模型相关

3.1 字符设备和块设备的区别,请分别列举一些实际的设备说出它们是属于哪一类设备?

3.2 请简述主设备号和次设备号的用途。如果执行mknod chartest c 4 64,创建chartest设备。请分析chartest使用的是哪一类设备驱动程序?

3.3 设备驱动程序中如何注册一个字符设备?分别解释下它的几个参数的含义?

3.4 字符型驱动设备怎么创建文件?

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

3.6 设备驱动模型三个重要的成员是?platform总线的匹配规则是?在具体应用上要不要先注册驱动在注册设备?有先后顺序没?

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

3.8 查看驱动模块中打印信息应该使用什么命令?如何查看内核中已有的字符设备的信息?如何查看正在使用的哪些中断号?

4 Bus相关

4.1 I2C

4.1.1 I2C总线协议和时序

IIC标准速率为100Kbit/s,快速模式400Kbit/s,支持多机通信,支持多主控模块,但是同一时刻只允许有一个主控。由数据线SDA和时钟SCL构成串行总线;每个电路模块都有唯一地址。

4.1.1.1 总线空闲状态

I2C总线的SDA和SCL两条信号线同时处于高电平时,规定为总线的空闲状态。此时各个器件的输出级的场效应管均处于截止状态,即释放总线,由两条信号线各自的上拉电阻把电平拉高。

4.1.1.2 启动信号(start)

在时钟线SCL保持高电平期间,数据线SDA上的电平被拉低(负跳变),定义为I2C总线的启动信号,它标志着一次数据传输的开始。启动信号是由主控器主动建立的,在建立该信号之前 I2C 总线必须处于空闲状态。

4.1.1.3 停止信号(stop)

在时钟线SCL保持高电平期间,数据线SDA被释放,使得SDA返回高电平(即正跳变),称为I2C总线的停止信号,它标志着一次数据传输的终止。停止信号也是由主控器主动建立的,建立该信号之后,I2C总线将返回空闲状态。

4.1.1.4 数据位传送

在I2C总线上传送的每一位数据都有一个时钟脉冲相对应(或同步控制),即在SCL串行时钟的配合下,在SDA上逐位地串行传送每一位数据。进行数据传送时,在SCL呈现高电平期间,SDA上的电平必须保持稳定,低电平为数据0,高电平为数据1。只有在SCL为低电平期间,才允许SDA上的电平改变状态。

4.1.1.5 应答信号(ACK)

I2C总线上的所有数据都是以8位传送的,发送器每发送一个字节,就在时钟脉冲9期间释放数据线,由接收器反馈一个应答信号。应答信号为低电平时,规定为有效应答位(ACK简称应答位),表示接收器已经成功地接收了该字节;应答信号为高电平时,规定为非应答位(NACK),一般表示接收器接收该字节没有成功。对于反馈有效应答位ACK的要求是,接收器在第9个时钟脉冲之前的低电平期间将 SDA 线拉低,并且确保在该时钟的高电平期间为稳定的低电平。

如果接收器是主控器,则在它收到最后一个字节后,发送一个NACK信号,以通知被控发送器结束数据发送,并释放SDA线,以便主控接收器发送一个停止信号。

4.1.2 I2C通讯时序图

  • 写时序

  • 读时序

4.1.3 I2C总线体系结构

主要包括:IIC核心、IIC总线驱动、IIC设备驱动

  1. I2C总线驱动:对应一个SOC的IIC控制器
  2. I2C设备驱动:对应一个具体的IIC外设
  3. I2C核心:具体的外设挂载在具体的IIC控制器上,因此IIC设备驱动需要和I2C总线驱动对应。因此需要I2C核心match。

4.1.3.1 相关结构体

  • struct i2c_adapter(I2C适配器)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    struct i2c_adapter {
    struct module *owner; // 所有者
    unsigned int id;
    unsigned int class; // 该适配器支持的从设备的类型
    const struct i2c_algorithm *algo; // 该适配器与从设备的通信算法
    void *algo_data;

    /* data fields that are valid for all devices */
    struct rt_mutex bus_lock;

    int timeout; // 超时时间
    int retries;
    struct device dev; // 该适配器设备对应的device

    int nr; // 适配器的编号
    char name[48]; // 适配器的名字
    struct completion dev_released;

    struct list_head userspace_clients; // 用来挂接与适配器匹配成功的从设备i2c_client的一个链表头
    };

    struct i2c_adapter是用来描述一个I2C适配器,在SoC中的指的就是内部外设I2C控制器,当向I2C核心层注册一个I2C适配器时就需要提供这样的一个结构体变量。

  • struct i2c_algorithm(I2C算法)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    struct i2c_algorithm {
    /* If an adapter algorithm can't do I2C-level access, set master_xfer
    to NULL. If an adapter algorithm can do SMBus access, set
    smbus_xfer. If set to NULL, the SMBus protocol is simulated
    using common I2C messages */
    /* master_xfer should return the number of messages successfully
    processed, or a negative value on error */
    int (*master_xfer)(struct i2c_adapter *adap, struct i2c_msg *msgs,
    int num);
    int (*smbus_xfer) (struct i2c_adapter *adap, u16 addr,
    unsigned short flags, char read_write,
    u8 command, int size, union i2c_smbus_data *data);

    /* To determine what the adapter supports */
    u32 (*functionality) (struct i2c_adapter *);
    };

    struct i2c_algorithm结构体代表的是适配器的通信算法,在构建i2c_adapter结构体变量的时候会去填充这个元素。

  • struct i2c_client

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct i2c_client {    //  用来描述一个i2c次设备
    unsigned short flags; // 描述i2c次设备特性的标志位
    unsigned short addr; // i2c 次设备的地址

    char name[I2C_NAME_SIZE]; // i2c次设备的名字
    struct i2c_adapter *adapter; // 指向与次设备匹配成功的适配器
    struct i2c_driver *driver; // 指向与次设备匹配成功的设备驱动
    struct device dev; // 该次设备对应的device
    int irq; // 次设备的中断引脚
    struct list_head detected; // 作为一个链表节点挂接到与他匹配成功的i2c_driver 相应的链表头上
    };
  • struct device_driver

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    struct i2c_driver {    // 代表一个i2c设备驱动
    unsigned int class; // i2c设备驱动所支持的i2c设备的类型

    /* Notifies the driver that a new bus has appeared or is about to be
    * removed. You should avoid using this if you can, it will probably
    * be removed in a near future.
    */
    int (*attach_adapter)(struct i2c_adapter *); // 用来匹配适配器的函数 adapter
    int (*detach_adapter)(struct i2c_adapter *);

    /* Standard driver model interfaces */
    int (*probe)(struct i2c_client *, const struct i2c_device_id *); // 设备驱动层的probe函数
    int (*remove)(struct i2c_client *); // 设备驱动层卸载函数

    /* driver model interfaces that don't relate to enumeration */
    void (*shutdown)(struct i2c_client *);
    int (*suspend)(struct i2c_client *, pm_message_t mesg);
    int (*resume)(struct i2c_client *);

    /* Alert callback, for example for the SMBus alert protocol.
    * The format and meaning of the data value depends on the protocol.
    * For the SMBus alert protocol, there is a single bit of data passed
    * as the alert response's low bit ("event flag").
    */
    void (*alert)(struct i2c_client *, unsigned int data);

    /* a ioctl like command that can be used to perform specific functions
    * with the device.
    */
    int (*command)(struct i2c_client *client, unsigned int cmd, void *arg);

    struct device_driver driver; // 该i2c设备驱动所对应的device_driver
    const struct i2c_device_id *id_table; // 设备驱动层用来匹配设备的id_table

    /* Device detection callback for automatic device creation */
    int (*detect)(struct i2c_client *, struct i2c_board_info *);
    const unsigned short *address_list; // 该设备驱动支持的所有次设备的地址数组
    struct list_head clients; // 用来挂接与该i2c_driver匹配成功的i2c_client (次设备)的一个链表头
    };
  • struct i2c_board_info

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    //  这个结构体是用来描述板子上的一个i2c设备的信息
    struct i2c_board_info {
    char type[I2C_NAME_SIZE]; //设备名,最长20个字符,最终安装到client的name上
    unsigned short flags; //最终安装到client.flags
    unsigned short addr; //设备从地址slave address,最终安装到client.addr上
    void *platform_data; //设备数据,最终存储到i2c_client.dev.platform_data上
    struct dev_archdata *archdata;
    struct device_node *of_node; //OpenFirmware设备节点指针
    struct acpi_dev_node acpi_node;
    int irq; //设备采用的中断号,最终存储到i2c_client.irq上
    };

    struct i2c_devinfo {
    struct list_head list; // 作为一个链表节点挂接到__i2c_board_list 链表上去
    int busnum; // 适配器的编号
    struct i2c_board_info board_info; // 内置的i2c_board_info 结构体
    };

4.1.3.2 I2C设备驱动

负责实现i2c_driver(包含一套驱动方法,操作具体的I2C外部设备方法)和i2c_client(包含具体的I2C外部设备的硬件信息,还有一些内核自动填充的信息)两个数据结构。一个i2c_driver可以驱动多个同类i2c_client。

设备驱动的实现方法有两种:

  • i2c-dev:这种方法只封装了操作SOC中I2C控制器的一些方法。需要在用户层直接控制硬件IIC,属于“应用层驱动”
  • 在驱动层封装所有的设备驱动方法,向用户层提供最终的操作结果,硬件的操作在驱动层完成。(常用)

图注:I2C控制器本身挂载在Platform总线上。由i2c_board_info描述。

4.1.3.3 I2C核心

I2C总线驱动和IIC设备驱动的注册和注销,I2C上层通信代码实现,探测设备,检测设备地址和上层代码实现。实现设备和I2C控制器的分离。

4.1.3.4 I2C总线驱动

实现IIC适配器数据结构【i2c_adapter对应一个SOC里面硬件的IIC控制器】,i2c适配器的algorithm数据结构【i2c_algorithm实现IIC通信方法】。

4.1.4 I2C设备驱动编写

IIC驱动编写和一般字符设备编写API区别和工作流程:

5 Debug相关

5.1 Kernel Oops和Panic是一回事吗

ops英文单词的中文含义是“哎呀”,表示“惊叹”;Panic英文单词的中文含义是“惊慌”。所以panic的程度显然是高于oops的,因为惊叹不一定会惊慌,而惊慌最容易失措,内核panic后,就死机了,俗称内核崩溃。但是内核报oops,这个时候不见得会panic,它可能只是报个oops,杀死进程而已。

从代码逻辑可以看出,当这个oops发生的时候,如何in_interrupt()成立,或者panic_on_oops成立,都是直接panic(),否则只是以一个信号退出进程而已。

由此可见,如果我们在一个中断上下文,这个oops必须抛panic;否则如果只是一个进程上下文,打印个oops,进程退出即可。另外,如果/proc/sys/kernel/panic_on_oops设置的值是1,这个时候,不管你在什么上下文,都是要panic的。

  • 在硬中断;

  • 在软中断(soft irq);

  • 在NMI

    NMI (Non Maskable Interrupt)——不可屏蔽中断(即CPU不能屏蔽)

    不可屏蔽中断请求信号NMI用来通知CPU,发生了“灾难性”的事件,如电源掉电、存储器读写出错、总线奇偶位出错等。NMI线上中断请求是不可屏蔽的(即无法禁止的)、而且立即被CPU锁存。因此NMI是边沿触发,不需要电平触发。NMI的优先级也比INTR高。不可屏蔽中断的类型指定为2,在CPU响应NMI时,不必由中断源提供中断类型码,因此NMI响应也不需要执行总线周期INTA

5.2 Kernel Panic

了解kernel panic 流程

6 同步相关

6.1 linux中的同步机制?

6.1.1 自旋锁

得不到资源时,会原地打转,知道获取资源为止。

使用步骤如下:

  1. 定义自旋锁

    1
    spinlock_t spin;
  2. 初始化自旋锁

    1
    spin_lock_init(lock);
  3. 获取自旋锁

    1
    2
    3
    4
    //获得自旋锁,如果能立即获得,则马上返回,否则自旋在那里,直到该自旋锁的保持者释放
    spin_lock(lock);
    //尝试获得自旋锁,如果能立即获得,它获得并返回真,否则立即返回假,实际上,不再“在原地打转”
    spin_trylock(lock);
  4. 释放自旋锁

    1
    spin_unlock(lock)

使用示例:

1
2
3
4
5
spinlock_t lock;
spin_lock_init(&lock);
spin_lock(&lock);//获取自旋锁,保护临界区
...//临界区
spin_unlock(&lock);//解锁

6.1.2 互斥锁

  1. 定义并初始化互斥锁

    1
    2
    struct mutex my_mutex;
    mutex_init(&my_mutex);
  2. 获取互斥锁

    1
    2
    3
    void mutex_lock(struct mutex *lock);
    int mutex_lock_interruptible(strutct mutex *lock);
    int mutex_trylock(struct mutex *lock);
  3. 释放互斥锁

    1
    void mutex_unlock(struct mutex *lock);

使用示例

1
2
3
4
5
6
struct mutex my_mutex;
mutex_init(&my_mutex);

mutex_lock(&my_mutex);
...//临界资源
mutex_unlock(&my_mutex);

6.1.3 信号量

得不到资源,会进入休眠状态

使用步骤如下:

  1. 定义信号量

    1
    struct semaphore sem;
  2. 初始化信号量

    1
    2
    3
    void sema_init(struct semaphore *sem,int val);	//初始化并设置为val
    void init_MUTEX(struct semaphore *sem); //初始化并设置为1
    void init_MUTEX_LOCKED(struct semaphore *sem); //初始化并设置为0

    下面两个宏用于定义并初始化信号量的“快捷方式”

    1
    2
    DECLARE_MUTEX(name);		//初始化并设置为1
    DECLARE_MUTEX_LOCKED(name); //初始化并设置为0
  3. 获取信号量

    1
    2
    void down(struct semaphore *sem);				//会导致休眠,不能在中断上下文使用
    int down_interruptible(struct semaphore *sem); //不会导致休眠,可在中断上下文使用
  4. 释放信号量

    1
    void up(struct semaphore *sem);	//释放信号量sem,唤醒等待者

使用示例:

1
2
3
4
5
6
DECLARE_MUTEX(mount_sem);
down(&mount_sem);获取信号量,保护临界区
...
critical section //临界区
...
up(&mount_sem);//释放信号量

6.2 什么是死锁?如何避免死锁?

6.2.1 什么是死锁

​ 死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者由于彼此通信而造成的一种阻塞的现象,若无外力作用,它们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。

 例如,在某个计算机系统中只有一台打印机和一台输入 设备,进程P1正占用输入设备,同时又提出使用打印机的请求,但此时打印机正被进程P2 所占用,而P2在未释放打印机之前,又提出请求使用正被P1占用着的输入设备。这样两个进程相互无休止地等待下去,均无法继续执行,此时两个进程陷入死锁状态

6.2.2 死锁产生的原因

  1. 系统资源的竞争

  2. 进程运行推进顺序不当引起死锁

    • 进程推进顺序合法

      当进程P1和P2并发执行时,如果按照下述顺序推进:P1:Request(R1); P1:Request(R2); P1: Relese(R1);P1: Relese(R2); P2:Request(R2); P2:Request(R1); P2: Relese(R2);P2: Relese(R1);这两个进程便可顺利完成,这种不会引起进程死锁的推进顺序是合法的

    • 进程推进顺序非法

      若P1保持了资源R1,P2保持了资源R2,系统处于不安全状态,因为这两个进程再向前推进,便可能发生死锁。例如,当P1运行到P1:Request(R2)时,将因R2已被P2占用而阻塞;当P2运行到P2:Request(R1)时,也将因R1已被P1占用而阻塞,于是发生进程死锁。

6.2.3 产生死锁的四个必要条件

  • 互斥条件

    指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用毕释放。

  • 请求与保持条件

    进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。

  • 不可剥夺条件

    进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。

  • 循环等待条件

    指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{P0,P1,P2,···,Pn}中的P0正在等待一个P1占用的资源;P1正在等待P2占用的资源,……,Pn正在等待已被P0占用的资源。

这四个条件是死锁的必要条件,只要系统发生死锁,这些条件必然成立,而只要上述条件之一不满足,就不会发生死锁。

6.2.4 死锁的避免与预防

  理解了死锁的原因,尤其是产生死锁的四个必要条件,就可以最大可能地避免、预防和解除死锁。只要打破四个必要条件之一就能有效预防死锁的发生:

  • 打破互斥条件

    改造独占性资源为虚拟资源,大部分资源已无法改造。

  • 打破不可抢占条件

    当一进程占有一独占性资源后又申请一独占性资源而无法满足,则退出原占有的资源

  • 打破占有且申请条件

    采用资源预先分配策略,即进程运行前申请全部资源,满足则运行,不然就等待,这样就不会占有且申请。

  • 打破循环等待条件

    实现资源有序分配策略,对所有设备实现分类编号,所有进程只能采用按序号递增的形式申请资源。

死锁避免和死锁预防的区别:

​ 死锁预防是设法至少破坏产生死锁的四个必要条件之一,严格的防止死锁的出现;而死锁避免则不那么严格的限制产生死锁的必要条件的存在,因为即使死锁的必要条件存在,也不一定发生死锁。死锁避免是在系统运行过程中注意避免死锁的最终发生。

6.3 驱动里面为什么要有并发、互斥的控制?如何实现?讲个例子?

并发(concurrency)指的是多个执行单元同时、并行被执行,而并发的执行单元对共享资源(硬件资源和软件上的全局变量、静态变量等)的访问则很容易导致竞态(race conditions)。

​ 解决竞态问题的途径是保证对共享资源的互斥访问,所谓互斥访问就是指一个执行单元 在访问共享资源的时候,其他的执行单元都被禁止访问。

​ 访问共享资源的代码区域被称为临界区,临界区需要以某种互斥机制加以保护,中断屏蔽,原子操作,自旋锁,和信号量都是linux设备驱动中可采用的互斥途径。

7 中断相关

7.1 系统软中断、tasklet、工作队列work queue的区别及使用

中断上半部:

  • 对时间要求比较高的工作
  • 硬件相关的操作
  • 不能被中断打断,因为进入中断时候一般都会禁止本地CPU中断

中断下半部:

  • 可延迟执行的操作(对时间要求不高)

7.1.1 软中断

目前Linux系统最多支持32个软中断,系统已经定义使用了10个,剩下的用户可以自己指定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
enum
{
HI_SOFTIRQ=0,
TIMER_SOFTIRQ,
NET_TX_SOFTIRQ,
NET_RX_SOFTIRQ,
BLOCK_SOFTIRQ,
BLOCK_IOPOLL_SOFTIRQ,
TASKLET_SOFTIRQ,
SCHED_SOFTIRQ,
HRTIMER_SOFTIRQ,
RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

NR_SOFTIRQS
};

上面列出的软中断类型越靠前优先级越高,其中有两个需要关注一下,就是HI_SOFTIRQ和TASKLET_SOFTIRQ,系统已经帮我们初始化好了,tasklet就是基于这两个软中断去实现的。具体代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
//init/main.c
asmlinkage __visible void __init start_kernel(void)
{
char *command_line;
char *after_dashes;

/*
* Need to run as early as possible, to initialize the
* lockdep hash:
*/
lockdep_init();
set_task_stack_end_magic(&init_task);
smp_setup_processor_id();
debug_objects_early_init();

/*
* Set up the the initial canary ASAP:
*/
boot_init_stack_canary();

cgroup_init_early();

local_irq_disable();
early_boot_irqs_disabled = true;

.... ....

/*
* These use large bootmem allocations and must precede
* kmem_cache_init()
*/
setup_log_buf(0);
pidhash_init();
vfs_caches_init_early();
sort_main_extable();
trap_init();
mm_init();
.... ....
early_irq_init();
init_IRQ();
tick_init();
rcu_init_nohz();
init_timers();
hrtimers_init();
softirq_init();//初始化软中断
timekeeping_init();
time_init();
....
....
....
#ifdef CONFIG_X86_ESPFIX64
/* Should be run before the first non-init thread is created */
init_espfix_bsp();
#endif
thread_info_cache_init();
cred_init();
fork_init();
.... ....

check_bugs();

acpi_subsystem_init();
sfi_init_late();

if (efi_enabled(EFI_RUNTIME_SERVICES)) {
efi_late_init();
efi_free_boot_services();
}

ftrace_init();

/* Do the rest non-__init'ed, we're now alive */
rest_init();
}


//kernel/softirq.c
void __init softirq_init(void)
{
int cpu;

for_each_possible_cpu(cpu) {
per_cpu(tasklet_vec, cpu).tail =
&per_cpu(tasklet_vec, cpu).head;
per_cpu(tasklet_hi_vec, cpu).tail =
&per_cpu(tasklet_hi_vec, cpu).head;
}

open_softirq(TASKLET_SOFTIRQ, tasklet_action);
open_softirq(HI_SOFTIRQ, tasklet_hi_action);
}

其中:

open_softirq(TASKLET_SOFTIRQ, tasklet_action);

open_softirq(HI_SOFTIRQ, tasklet_hi_action);

就是系统为我们初始化好的和tasklet相关的软中断。

7.1.1.1 自定义软中断

我们也可以自己定义属于自己的软中断,方法如下:

  1. 添加我们自己的软中断

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    enum
    {
    HI_SOFTIRQ=0,
    TIMER_SOFTIRQ,
    NET_TX_SOFTIRQ,
    NET_RX_SOFTIRQ,
    BLOCK_SOFTIRQ,
    BLOCK_IOPOLL_SOFTIRQ,
    TASKLET_SOFTIRQ,
    SCHED_SOFTIRQ,
    HRTIMER_SOFTIRQ,
    MY_SOFTIRQ, /*我自己添加的软中断*/
    RCU_SOFTIRQ, /* Preferable RCU should always be the last softirq */

    NR_SOFTIRQS
    };
  2. 在kernel/softirq.c中定义自己的软中断处理函数

    1
    2
    3
    4
    5
    //我自己定义的软中断处理函数
    static void my_softirq_action(struct softirq_action *a)
    {
    ...
    }
  3. 初始化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    void __init softirq_init(void)
    {
    int cpu;

    for_each_possible_cpu(cpu) {
    per_cpu(tasklet_vec, cpu).tail =
    &per_cpu(tasklet_vec, cpu).head;
    per_cpu(tasklet_hi_vec, cpu).tail =
    &per_cpu(tasklet_hi_vec, cpu).head;
    }

    open_softirq(TASKLET_SOFTIRQ, tasklet_action);
    open_softirq(HI_SOFTIRQ, tasklet_hi_action);
    open_softirq(MY_SOFTIRQ, tasklet_hi_action);//我自己定义的软中断
    }
  4. 激活

    1
    raise_softirq(MY_SOFTIRQ);

以上就是自己定义的软中断的流程。

7.1.1.2 软中断的执行

软中断的执行既可以守护进程(ksoftirqd)中执行,也可以在中断的退出阶段执行。实际上,软中断更多的是在中断的退出阶段执行(irq_exit),以便达到更快的响应,加入守护进程机制,只是担心一旦有大量的软中断等待执行,会使得内核过长地留在中断上下文中。

7.1.1.2.1 在irq_exit中执行
1
2
3
4
5
6
7
8
void irq_exit(void)
{
......
sub_preempt_count(IRQ_EXIT_OFFSET);
if (!in_interrupt() && local_softirq_pending())
invoke_softirq();
......
}

如果中断发生嵌套,in_interrupt()保证了只有在最外层的中断的irq_exit阶段,invoke_interrupt才会被调用,当然,local_softirq_pending也会实现判断当前cpu有无待决的软中断。代码最终会进入do_softirq中,内核会保证调用do_softirq时,本地cpu的中断处于关闭状态,进入__do_softirq:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
asmlinkage void __do_softirq(void)
{
......
pending = local_softirq_pending();

__local_bh_disable((unsigned long)__builtin_return_address(0),
SOFTIRQ_OFFSET);
restart:
/* Reset the pending bitmask before enabling irqs */
set_softirq_pending(0);

local_irq_enable();

h = softirq_vec;

do {
if (pending & 1) {
......
trace_softirq_entry(vec_nr);
h->action(h);
trace_softirq_exit(vec_nr);
......
}
h++;
pending >>= 1;
} while (pending);

local_irq_disable();

pending = local_softirq_pending();
if (pending && --max_restart)
goto restart;

if (pending)
wakeup_softirqd();

lockdep_softirq_exit();

__local_bh_enable(SOFTIRQ_OFFSET);
}
  • 首先取出pending的状态;
  • 禁止软中断,主要是为了防止和软中断守护进程发生竞争;
  • 清除所有的软中断待决标志
  • 打开本地cpu中断;
  • 循环执行待决软中断的回调函数
  • 如果循环完毕,发现新的软中断被触发,则重新启动循环,直到以下条件满足,才退出:
    • 没有新的软中断等待执行
    • 循环已经达到最大的循环次数MAX_SOFTIRQ_RESTART,目前的设定值时10次;
  • 如果经过MAX_SOFTIRQ_RESTART次循环后还未处理完,则激活守护进程,处理剩下的软中断;
  • 退出前恢复软中断
7.1.1.2.2 在ksoftirqd进程中执行

软中断也可能由ksoftirqd守护进程执行,这要发生在以下两种情况下:

  • 在irq_exit中执行软中断,但是在经过MAX_SOFTIRQ_RESTART次循环后,软中断还未处理完,这种情况虽然极少发生,但毕竟有可能;
  • 内核的其它代码主动调用raise_softirq,而这时正好不是在中断上下文中,守护进程将被唤醒;

守护进程最终也会调用__do_softirq执行软中断的回调,具体的代码位于run_ksoftirqd函数中,内核会关闭抢占的情况下执行__do_softirq。

7.1.2 tasklet

因为内核已经定义好了10种软中断类型,并且不建议我们自行添加额外的软中断,所以对软中断的实现方式,我们主要是做一个简单的了解,对于驱动程序的开发者来说,无需实现自己的软中断。但是,对于某些情况下,我们不希望一些操作直接在中断的handler中执行,但是又希望在稍后的时间里得到快速地处理,这就需要使用tasklet机制。 tasklet是建立在软中断上的一种延迟执行机制,它的实现基于TASKLET_SOFTIRQ和HI_SOFTIRQ这两个软中断类型。

tasklet示例如下:

  1. 定一个tasklet类型的结构体变量

    1
    2
    /*使用tasklet机制的中断下半部*/
    struct tasklet_struct my_tasklet;
  2. 编写软中断处理函数

    1
    2
    3
    static void my_tasklet_fun(unsigned long arg){
    ...
    }
  3. 初始化,将tasklet软中断处理函数和tasklet挂钩

    1
    tasklet_init(my_tasklet,my_tasklet_fun,(unsigned long) sport);
  4. 调用tasklet_schedule触发调度tasklet

    1
    tasklet_schedule(&sport->my_tasklet);//调度tasklet

定义tasklet变量,实现软中断处理函数,初始化,调度,以上这些就是tasklet的使用步骤了,内核帮我们省略了很多麻烦的实现,所以使用起来比较简单。

7.1.3 工作队列work queue

软中断和tasklet是运行于中断上下文的,它们属于内核态没有进程的切换,因此在执行过程中不能休眠,不能阻塞,一旦休眠或者阻塞,则系统直接挂死。因此软中断和tasklet是有一定的使用局限性的,工作队列的出现正是用在软中断和tasklet不能使用的场合,比如需要调用一个具有可延迟函数的特质,但是这个函数又有可能引起休眠、阻塞。

工作队列的使用步骤如下:

  1. 定义一个工作队列对象

    1
    2
    /*工作队列机制*/
    struct work_struct my_work;
  2. 编写工作队列处理函数

    1
    2
    3
    4
    /*工作队列机制*/
    static void my_work_fun(struct work_struct *w){
    ...
    }
  3. 初始化工作队列

    1
    INIT_WORK(&my_work, my_work_fun);
  4. 调度工作队列

    1
    schedule_work(&my_work);

7.2 linux中断响应的执行流程

这个序列图展示了整个通用中断子系统的中断响应过程,flow_handle一栏就是中断流控层的生命周期

7.3 中断注册函数和中断注销函数

1
2
3
4
5
6
request_threaded_irq(unsigned int irq,
irq_handler_t handler,
irq_handler_t thread_fn,
unsigned long flags,
const char *name,
void *dev);
  • irq

    需要申请的irq编号,对于ARM体系,irq编号通常在平台级的代码中事先定义好,有时候也可以动态申请。

  • handler

    中断服务回调函数,该回调运行在中断上下文中,并且cpu的本地中断处于关闭状态,所以该回调函数应该只是执行需要快速响应的操作,执行时间应该尽可能短小,耗时的工作最好留给下面的thread_fn回调处理。

  • thread_fn

    如果该参数不为NULL,内核会为该irq创建一个内核线程,当中断发生时,如果handler回调返回值是IRQ_WAKE_THREAD,内核将会激活中断线程,在中断线程中,该回调函数将被调用,所以,该回调函数运行在进程上下文中,允许进行阻塞操作。

  • flags

    控制中断行为的位标志。例如:IRQF_TRIGGER_RISING,IRQF_TRIGGER_LOW,IRQF_SHARED等,在include/linux/interrupt.h中定义。

  • name

    申请本中断服务的设备名称,同时也作为中断线程的名称,该名称可以在/proc/interrupts文件中显示。

  • dev

    当多个设备的中断线共享同一个irq时,它会作为handler的参数,用于区分不同的设备。

7.4 中断和轮询哪个效率高?怎样决定是采用中断方式还是采用轮询方式去实现驱动?

中断是CPU处于被动状态下来接受设备的信号,而轮询是CPU主动去查询该设备是否有请求。凡事都是两面性,所以,看效率不能简单的说那个效率高。如果是请求设备是一个频繁请求cpu的设备,或者有大量数据请求的网络设备,那么轮询的效率是比中断高。如果是一般设备,并且该设备请求cpu的频率比较底,则用中断效率要高一些。主要是看请求频率

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

  • 中断处理例程应该尽量短把能放在后半段(tasklet,等待队列等)的任务尽量放在后半段
  • 中断服务程序中不能有阻塞操作。应为中断期间是完全占用CPU的(即不存在内核调度),中断被阻塞住,其他进程将无法操作;
  • 中断服务程序注意返回值,要用操作系统定义的宏做为返回值,而不是自己定义的OK,FAIL之类的。

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

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

7.7 IRQ和FIQ有什么区别?

一般的中断控制器里我们可以配置与控制器相连的某个中断输入是FIQ还是IRQ,所以一个中断是可以指定为FIQ或者IRQ的,为了合理,要求系统更快响应,自身处理所耗时间也很短的中断设置为FIQ,否则就设置了IRQ。

区别如下:

  • FIQ比IRQ有更高优先级
  • IRQ可以被FIQ所中断,但FIQ不能被IRQ所中断。
  • FIQ模式下,比IRQ模式多了几个独立的寄存器