嵌入式八股

1.SELECT和POLL的区别

selectpoll 都是用于 I/O 多路复用的系统调用,它们可以让一个进程可以监视多个文件描述符的状态变化,以便在有事件到达时进行处理。虽然它们有类似的功能,但存在一些区别:

  1. 可移植性

    • select:在很多 Unix 系统上都支持,但它有一些限制,如文件描述符数量的限制。
    • poll:也在许多 Unix 系统上支持,并且一般没有像 select 那样的文件描述符数量限制。它更加通用,支持更多的事件类型。
  2. 参数传递

    • select:使用三个位掩码数组来传递关注的文件描述符集合、可读集合、可写集合和异常集合。当其中一个集合中有就绪的文件描述符时,select 返回,然后需要遍历这些集合来判断具体哪些文件描述符就绪。
    • poll:使用一个 pollfd 结构体数组来传递文件描述符及关注的事件。当有文件描述符就绪时,poll 将会返回,并且已就绪的文件描述符会在相应的 revents 字段中标记。
  3. 扩展性

    • select:通常随着监视的文件描述符数量增加,性能会下降,因为 select 使用了线性搜索。
    • poll:相对于 selectpoll 在文件描述符数量增加时性能可能会更好,但在大量文件描述符时,仍然会存在性能问题。
  4. 事件类型

    • select:通常只支持可读、可写和异常事件。
    • poll:支持更多的事件类型,如可读、可写、异常、挂起等。 需要注意的是,selectpoll 都属于早期的 I/O 多路复用方法,现代系统中更常使用的是 epoll(Linux 环境下)或其他更高效的方法。epoll 具有更好的性能和扩展性,特别适用于大规模的连接和高并发的网络编程。

当有大量的文件描述符就绪时,selectpoll 可能会出现频繁的返回,导致频繁的遍历文件描述符集合,这种情况下可能会导致性能问题。

2. Web服务器如何接收客户端发来的HTTP请求报文呢?

#include <sys/socket.h>
#include <netinet/in.h>
/* 创建监听socket文件描述符 */
int listenfd = socket(PF_INET, SOCK_STREAM, 0);
/* 创建监听socket的TCP/IP的IPV4 socket地址 */
struct sockaddr_in address;
bzero(&address, sizeof(address));
address.sin_family = AF_INET;
address.sin_addr.s_addr = htonl(INADDR_ANY);  /* INADDR_ANY:将套接字绑定到所有可用的接口 */
address.sin_port = htons(port);

int flag = 1;
/* SO_REUSEADDR 允许端口被重复使用 */
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &flag, sizeof(flag));
/* 绑定socket和它的地址 */
ret = bind(listenfd, (struct sockaddr*)&address, sizeof(address));  
/* 创建监听队列以存放待处理的客户连接,在这些客户连接被accept()之前 */
ret = listen(listenfd, 5);

​ 远端的很多用户会尝试去connect()这个Web Server上正在listen的这个port,而监听到的这些连接会排队等待被accept()。由于用户连接请求是随机到达的异步事件,每当监听socket(listenfdlisten到新的客户连接并且放入监听队列,我们都需要告诉我们的Web服务器有连接来了,accept这个连接,并分配一个逻辑单元来处理这个用户请求。而且,我们在处理这个请求的同时,还需要继续监听其他客户的请求并分配其另一逻辑单元来处理(并发,同时处理多个事件,后面会提到使用线程池实现并发)。这里,服务器通过epoll这种I/O复用技术(还有select和poll)来实现对监听socket(listenfd)和连接socket(客户请求)的同时监听。注意I/O复用虽然可以同时监听多个文件描述符,但是它本身是阻塞的,并且当有多个文件描述符同时就绪的时候,如果不采取额外措施,程序则只能按顺序处理其中就绪的每一个文件描述符,所以为提高效率,我们将在这部分通过线程池来实现并发(多线程并发)处理多个就绪的文件描述符,为每个就绪的文件描述符分配一个逻辑单元(线程)来处理。

3. 虚拟内存的作用是什么

虚拟内存是一种计算机操作系统的内存管理技术,它的作用是扩展物理内存,为每个进程提供了一个抽象的、虚拟的内存空间。虚拟内存的主要作用包括:

  1. 提供更大的可用内存空间:虚拟内存允许一个进程访问比物理内存更大的内存空间。当物理内存不足时,操作系统可以将部分数据从内存中置换到磁盘上,从而为其他进程提供更多的可用内存。

  2. 隔离进程内存:每个进程都有自己的虚拟内存空间,进程之间的内存空间相互隔离,不会相互干扰。这提高了系统的稳定性和安全性,因为一个进程的错误不会直接影响其他进程。

  3. 简化编程:通过提供一个连续的虚拟内存地址空间,虚拟内存简化了编程,使程序员不需要考虑物理内存的具体位置和管理。

  4. 实现内存共享:虚拟内存使得操作系统可以实现内存共享,多个进程可以共享相同的内存页面,从而节省物理内存空间。

  5. 延迟加载:虚拟内存允许将程序的代码和数据从磁盘逐页加载到内存中,只有在需要时才加载,这可以减少程序启动时间和内存消耗。

  6. 内存映射文件:虚拟内存允许将磁盘上的文件映射到内存中的一个区域,从而可以像访问内存一样访问文件内容,提高了文件 I/O 的效率。

虚拟内存的引入使得操作系统更加灵活地管理内存资源,提高了系统的性能和可用性。然而,虚拟内存也需要操作系统进行复杂的内存管理,包括页面置换、页面映射、页表管理等,从而带来一定的性能开销。

4. STM32MP157开发板

​ STM32MP157A 处理器。STM32MP157A 是基于 Cortex-A7 32bit RISC核心加 Cortex-M4 32bit RISC 核心的高性能双核心处理器。最高工作频率为 800MHz。Cortex-A7 处理器为每个 CPU 内置一个 32 kbyte L1 指令缓存,一个 32 kbyte L1 数据缓存为每个 CPU 内
置一个 256 kbyte 2 级缓存。同时 Cortex-A7 处理器是一个低功耗的应用处理器,旨在为高端可穿戴设备以及其他低功耗嵌入式和消费应用提供丰富的性能。它提供了比 Cortex-A5多 20%的单线程性能。

​ STM32MP157A 处理器还嵌入 Cortex-M4 32bit RISC 核心,最高可工作在 209 MHz 频率。Cortex-M4 核心具有浮点单元(FPU)单精度,支持单精度数据处理指令和数据类型。Cortex-M4 支持一套完整的 DSP 指令和一个增强应用安全性的内存保护单元(MPU)。

5. ARM异常及中断处理介绍(中断是异常的一种)

中断

概念:是一个过程,是CPU在执行当前程序的过程中因硬件或软件的原因插入了另一段程序运行的过程。因硬件原因引起的中断过程的出现时不可预测的,即随机的,而软中断是事先安排好的。

中断源

概念:可以引起中断的信号源。

异常优先级

ARM处理器中有7种类型的异常,按优先级从高到低的排列如下:复位异常(Reset)数据异常(Data Abort)、快速中断异常(FIQ)、外部中断异常(IRQ)、预取异常(Prefetch Abort)、软中断异常(SWI)、未定义指令异常(Undefined interrupt)。
优先级最低的两种异常是软件中断异常和未定义指令异常。因为正在执行的指令不可能既是一条软中断指令,又是一条未定义指令,所以软中断异常和未定义指令异常享有相同的优先级。

注意

在ARM处理器中(Exception)和中断(Interrupt)有些差别,异常主要是从处理器被动接受异常的角度出发,而中断带有向处理器主动申请的色彩。此文中对“异常”和“中断”不做严格区分;两者都是指请求处理器打断正常的程序执行流程,进入特定程序循环的一种机制。

异常

概念:异常由内部或外部源产生并引起处理器处理一个事件。在处理异常之前,处理器状态必须保留,一遍在异常处理程序完成后,原来的程序能够重新执行。同一时刻可能出现多个异常。
注意

当异常出现时,异常模式分组的R14和SPSR用于保存状态。
当处理异常返回时,把SPSR传送到CPSR,R14传送到PC。

异常向量表

概念:当异常发生时,处理器会把PC设置为一个特定的存储器地址(强制从异常类型对应的固定存储地址开始执行程序)。用来记录哪种异常对应哪个地址的表。
特点:

就是一块内存空间,大小为32bytes,平均被分成了8份,每一份对应一个异常源,其中有一份是预留的;
在异常向量表中的每一份存放的是一个跳转指令,跳转到对应的异常处理函数;
异常向量表中每一份的位置是固定的,不能更改;
一般情况下,像启动程序(uboot)起始位置就是异常向量表;

6. 缺页异常属于ARM架构下的哪种类型异常

缺页异常属于ARM架构下的一种异常,通常被称为“数据中止异常”(Data Abort Exception)。这种异常是由于访问无效或未映射的内存地址导致的,也就是所谓的“缺页”。

当程序尝试访问一个无效的内存地址时,例如访问尚未分配的内存或者访问被限制的内存区域,就会触发缺页异常。操作系统通常会捕获这个异常,然后进行相应的处理,例如分配内存、映射虚拟内存等。在缺页异常处理完成后,程序可能会被恢复到触发异常的指令处继续执行,或者在一些情况下会终止程序的执行。

需要注意的是,不同的ARM架构版本和操作系统可能会有一些差异,但一般来说,缺页异常属于ARM架构下的数据中止异常,用于处理内存访问异常情况。

7. 软中断

软中断(Software Interrupt),也被称为系统调用(System Call),是由程序中的特殊指令触发的一种中断机制。软中断允许用户程序请求操作系统提供特定的服务或功能,如文件操作、内存分配、进程管理等。下面是一些软中断的示例:

  1. 文件操作:在用户程序中,需要进行文件读写操作时,可以通过软中断请求操作系统的文件服务。
  2. 内存分配与释放:当用户程序需要动态分配内存或释放已分配的内存时,可以通过软中断调用操作系统的内存管理服务。
  3. 进程管理:用户程序可能需要创建新进程、销毁进程或进行进程间通信。这些操作通常需要通过软中断请求操作系统提供支持。
  4. 时间管理:用户程序可能需要获取系统时间、等待一段时间、设置定时器等功能,这些功能也可以通过软中断实现。
  5. 网络操作:在用户程序中,需要进行网络通信时,可以通过软中断请求操作系统的网络协议栈服务。
  6. 硬件访问:有时用户程序需要直接访问硬件设备,但这通常需要操作系统进行中介,以确保正确的权限和资源管理。

在不同的操作系统中,软中断的实现和调用方式可能会有所不同。在Linux操作系统中,使用系统调用号来唯一标识不同的服务或功能,用户程序可以使用特定的汇编指令(例如int 0x80syscall)触发软中断。操作系统在收到软中断请求后,会根据系统调用号来执行相应的操作。

8.Cortex-M架构的CPU为什么不支持linux系统

Linux操作系统依赖于虚拟内存管理,可以将物理内存和虚拟内存映射起来,实现内存隔离和共享。而Cortex-M架构通常不具备硬件支持虚拟内存的功能。

9.MPU和MMU的区别

MPU(Memory Protection Unit)和MMU(Memory Management Unit)都是用于内存管理的硬件单元,但在功能和应用上有一些区别。

  1. 功能

    • MPU(Memory Protection Unit):MPU主要用于内存保护,它可以设置不同区域的内存访问权限,以防止程序或进程越界访问内存。MPU的主要目的是提供严格的内存隔离和保护,确保每个进程只能访问它被授权访问的内存区域。MPU通常被用于嵌入式系统和实时操作系统中。
    • MMU(Memory Management Unit):MMU主要用于虚拟内存管理,它将虚拟内存地址映射到物理内存地址,使得每个进程都可以拥有连续的地址空间,而不受物理内存的限制。MMU的主要目的是为了提供更大的内存空间、内存共享和更灵活的内存分配。MMU通常被用于通用操作系统,如Windows、Linux等。
  2. 应用领域

    • MPU:主要用于嵌入式系统,特别是实时系统,需要确保对内存的访问受到严格的控制和保护。
    • MMU:主要用于通用计算机系统,为操作系统提供虚拟内存功能,使得多个进程可以在同一台机器上独立运行,而不会相互干扰。
  3. 内存访问控制

    • MPU:提供了精确的内存保护,可以对每个内存区域设置读、写、执行权限,并且可以防止越界访问。
    • MMU:主要关注虚拟内存的映射,内存保护是通过操作系统实现的,较为灵活,但可能没有MPU那么精确。
  4. 硬件要求

    • MPU:通常较简单,功能相对有限,适用于资源受限的系统。
    • MMU:较为复杂,需要支持虚拟内存映射、分页等功能,适用于通用计算机系统。

综上所述,MPU和MMU在内存管理的重点和功能上有所不同,根据应用场景的需求,选择适合的内存管理硬件单元。

10.Linux下有三种IO复用方式:epoll,select和poll,为什么用epoll,它和其他两个有什么区别呢?

  • 对于select和poll来说,所有文件描述符都是在用户态被加入其文件描述符集合的,每次调用都需要将整个集合拷贝到内核态;epoll则将整个文件描述符集合维护在内核态,每次添加文件描述符的时候都需要执行一个系统调用。系统调用的开销是很大的,而且在有很多短期活跃连接的情况下,epoll可能会慢于select和poll由于这些大量的系统调用开销。
  • select使用线性表描述文件描述符集合,文件描述符有上限;poll使用链表来描述;epoll底层通过红黑树来描述,并且维护一个ready list,将事件表中已经就绪的事件添加到这里,在使用epoll_wait调用时,仅观察这个list中有没有数据即可。
  • select和poll的最大开销来自内核判断是否有文件描述符就绪这一过程:每次执行select或poll调用时,它们会采用遍历的方式,遍历整个文件描述符集合去判断各个文件描述符是否有活动;epoll则不需要去以这种方式检查,当有活动产生时,会自动触发epoll回调函数通知epoll文件描述符,然后内核将这些就绪的文件描述符放到之前提到的ready list中等待epoll_wait调用后被处理。
  • select和poll都只能工作在相对低效的LT模式下,而epoll同时支持LT和ET模式。
  • 综上,当监测的fd数量较小,且各个fd都很活跃的情况下,建议使用select和poll;当监听的fd数量较多,且单位时间仅部分fd活跃的情况下,使用epoll会明显提升性能。

Epoll对文件操作符的操作有两种模式:LT(电平触发)和ET(边缘触发),二者的区别在于当你调用epoll_wait的时候内核里面发生了什么:

  • LT(电平触发):类似select,LT会去遍历在epoll事件表中每个文件描述符,来观察是否有我们感兴趣的事件发生,如果有(触发了该文件描述符上的回调函数),epoll_wait就会以非阻塞的方式返回。若该epoll事件没有被处理完(没有返回EWOULDBLOCK),该事件还会被后续的epoll_wait再次触发。
  • ET(边缘触发):ET在发现有我们感兴趣的事件发生后,立即返回,并且sleep这一事件的epoll_wait,不管该事件有没有结束。

在使用ET模式时,必须要保证该文件描述符是非阻塞的(确保在没有数据可读时,该文件描述符不会一直阻塞);并且每次调用readwrite的时候都必须等到它们返回EWOULDBLOCK(确保所有数据都已读完或写完)。

11. Web服务器如何处理以及响应接收到的HTTP请求报文呢?

该项目使用线程池(半同步半反应堆模式)并发处理用户请求,主线程负责读写,工作线程(线程池中的线程)负责处理逻辑(HTTP请求报文的解析等等)。通过之前的代码,我们将listenfd上到达的connection通过 accept()接收,并返回一个新的socket文件描述符connfd用于和用户通信,并对用户请求返回响应,同时将这个connfd注册到内核事件表中,等用户发来请求报文。这个过程是:通过epoll_wait发现这个connfd上有可读事件了(EPOLLIN),主线程就将这个HTTP的请求报文读进这个连接socket的读缓存中users[sockfd].read(),然后将该任务对象(指针)插入线程池的请求队列中pool->append(users + sockfd);,线程池的实现还需要依靠锁机制以及信号量机制来实现线程同步,保证操作的原子性。
在线程池部分做几点解释,然后大家去看代码的时候就更容易看懂了:

  • 所谓线程池,就是一个pthread_t类型的普通数组,通过pthread_create()函数创建m_thread_number线程,用来执行worker()函数以执行每个请求处理函数(HTTP请求的process函数),通过pthread_detach()将线程设置成脱离态(detached)后,当这一线程运行结束时,它的资源会被系统自动回收,而不再需要在其它线程中对其进行 pthread_join() 操作。
  • 操作工作队列一定要加锁(locker),因为它被所有线程共享。
  • 我们用信号量来标识请求队列中的请求数,通过m_queuestat.wait();来等待一个请求队列中待处理的HTTP请求,然后交给线程池中的空闲线程来处理。

为什么要使用线程池?

当你需要限制你应用程序中同时运行的线程数时,线程池非常有用。因为启动一个新线程会带来性能开销,每个线程也会为其堆栈分配一些内存等。为了任务的并发执行,我们可以将这些任务任务传递到线程池,而不是为每个任务动态开启一个新的线程。

:star::star:线程池中的线程数量是依据什么确定的?

在StackOverflow上面发现了一个还不错的回答,意思是:
线程池中的线程数量最直接的限制因素是中央处理器(CPU)的处理器(processors/cores)的数量N:如果你的CPU是4-cores的,对于CPU密集型的任务(如视频剪辑等消耗CPU计算资源的任务)来说,那线程池中的线程数量最好也设置为4(或者+1防止其他因素造成的线程阻塞);对于IO密集型的任务,一般要多于CPU的核数,因为线程间竞争的不是CPU的计算资源而是IO,IO的处理一般较慢,多于cores数的线程将为CPU争取更多的任务,不至在线程处理IO的过程造成CPU空闲导致资源浪费,公式:最佳线程数 = CPU当前可使用的Cores数 * 当前CPU的利用率 * (1 + CPU等待时间 / CPU处理时间)(还有回答里面提到的Amdahl准则可以了解一下)

每个read()后的HTTP请求是如何被处理的,我们直接看这个处理HTTP请求的入口函数:

void http_conn::process() {
    HTTP_CODE read_ret = process_read();
    if(read_ret == NO_REQUEST) {
        modfd(m_epollfd, m_sockfd, EPOLLIN);
        return;
    }
    bool write_ret = process_write(read_ret);
    if(!write_ret)
        close_conn();
    modfd(m_epollfd, m_sockfd, EPOLLOUT);
}

首先,process_read(),也就是对我们读入该connfd读缓冲区的请求报文进行解析。
HTTP请求报文由请求行(request line)、请求头部(header)、空行和请求数据四个部分组成有。两种请求报文(例子来自社长的详解文章
GET(Example)

GET /562f25980001b1b106000338.jpg HTTP/1.1
Host:img.mukewang.com
User-Agent:Mozilla/5.0 (Windows NT 10.0; WOW64)
AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.106 Safari/537.36
Accept:image/webp,image/*,*/*;q=0.8
Referer:http://www.imooc.com/
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-CN,zh;q=0.8
空行
请求数据为空

POST(Example,注意POST的请求内容不为空)

POST / HTTP1.1
Host:www.wrox.com
User-Agent:Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1; SV1; .NET CLR 2.0.50727; .NET CLR 3.0.04506.648; .NET CLR 3.5.21022)
Content-Type:application/x-www-form-urlencoded
Content-Length:40
Connection: Keep-Alive
空行
name=Professional%20Ajax&publisher=Wiley

GET和POST的区别

  • 最直观的区别就是GET把参数包含在URL中,POST通过request body传递参数。
  • GET请求参数会被完整保留在浏览器历史记录里,而POST中的参数不会被保留。
  • GET请求在URL中传送的参数是有长度限制。(大多数)浏览器通常都会限制url长度在2K个字节,而(大多数)服务器最多处理64K大小的url。
  • GET产生一个TCP数据包;POST产生两个TCP数据包。对于GET方式的请求,浏览器会把http header和data一并发送出去,服务器响应200(返回数据);而对于POST,浏览器先发送header,服务器响应100(指示信息—表示请求已接收,继续处理)continue,浏览器再发送data,服务器响应200 ok(返回数据)。

process_read()函数的作用就是将类似上述例子的请求报文进行解析,因为用户的请求内容包含在这个请求报文里面,只有通过解析,知道用户请求的内容是什么,是请求图片,还是视频,或是其他请求,我们根据这些请求返回相应的HTML页面等。项目中使用主从状态机的模式进行解析,从状态机(parse_line)负责读取报文的一行,主状态机负责对该行数据进行解析,主状态机内部调用从状态机,从状态机驱动主状态机。每解析一部分都会将整个请求的m_check_state状态改变,状态机也就是根据这个状态来进行不同部分的解析跳转的:

  • parse_request_line(text),解析请求行,也就是GET中的GET /562f25980001b1b106000338.jpg HTTP/1.1这一行,或者POST中的POST / HTTP1.1这一行。通过请求行的解析我们可以判断该HTTP请求的类型(GET/POST),而请求行中最重要的部分就是URL部分,我们会将这部分保存下来用于后面的生成HTTP响应。
  • parse_headers(text);,解析请求头部,GET和POST中空行以上,请求行以下的部分。
  • parse_content(text);,解析请求数据,对于GET来说这部分是空的,因为这部分内容已经以明文的方式包含在了请求行中的URL部分了;只有POST的这部分是有数据的,项目中的这部分数据为用户名和密码,我们会根据这部分内容做登录和校验,并涉及到与数据库的连接。

OK,经过上述解析,当得到一个完整的,正确的HTTP请求时,就到了do_request代码部分,我们需要首先对GET请求和不同POST请求(登录,注册,请求图片,视频等等)做不同的预处理,然后分析目标文件的属性,若目标文件存在、对所有用户可读且不是目录时,则使用mmap将其映射到内存地址m_file_address处,并告诉调用者获取文件成功。

抛开mmap这部分,先来看看这些不同请求是怎么来的:
假设你已经搭好了你的HTTP服务器,然后你在本地浏览器中键入localhost:9000,然后回车,这时候你就给你的服务器发送了一个GET请求,什么都没做,然后服务器端就会解析你的这个HTTP请求,然后发现是个GET请求,然后返回给你一个静态HTML页面,也就是项目中的judge.html页面,那POST请求怎么来的呢?这时你会发现,返回的这个judge页面中包含着一些新用户已有账号这两个button元素,当你用鼠标点击这个button时,你的浏览器就会向你的服务器发送一个POST请求,服务器段通过检查action来判断你的POST请求类型是什么,进而做出不同的响应。

/* judge.html */
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title>WebServer</title>
    </head>
    <body>
    <br/>
    <br/>
    <div align="center"><font size="5"> <strong>欢迎访问</strong></font></div>
    <br/>
        <br/>
        <form action="0" method="post">
            <div align="center"><button type="submit">新用户</button></div>
        </form>
        <br/>
        <form action="1" method="post">
            <div align="center"><button type="submit" >已有账号</button></div>
        </form>

        </div>
    </body>
</html>

线程池的线程挂掉了,怎么办

线程池中线程挂掉(崩溃或异常终止)是一个常见的情况。以下是一些处理线程池中线程故障的一般方法:

  1. 重新启动线程池: 最简单的方法是重新启动整个线程池,以替代已经挂掉的线程。这通常需要一个监视机制,能够检测到线程崩溃,并启动一个新线程来替代它。这种方法简单,但可能会导致线程池的停滞,因为在重新启动之前需要一些时间。

  2. 自动重启线程: 另一种方法是让线程在崩溃后自动重启。这需要在线程中添加一些逻辑,以便在异常发生时捕获异常并重新启动线程。这需要更多的编程工作,但可以避免整个线程池的停滞。

  3. 监控线程健康状态: 可以实现一个监控线程,定期检查线程的健康状态。如果监控线程发现某个线程不正常,可以采取适当的措施,如重新启动或替换线程。

  4. 记录和报警: 在线程池中发生线程故障时,应该记录相关的错误信息,并可以选择发送警报通知相关人员进行手动处理。这可以帮助识别和解决潜在问题。

  5. 优化线程池: 有时线程池中的线程频繁崩溃可能是因为资源不足、线程池配置不合理等原因。检查线程池的配置,确保足够的资源分配给线程池。

  6. 处理异常: 在编写线程池的代码时,要充分处理异常情况,以避免线程崩溃。这包括使用try-catch块来捕获异常,处理资源泄漏等。

总之,处理线程池中线程挂掉的方法取决于你的具体应用场景和需求。通常,结合上述多种方法可以有效地处理线程池中线程的故障,确保线程池的稳定运行。

12. 数据库连接池是如何运行的

在处理用户注册,登录请求的时候,我们需要将这些用户的用户名和密码保存下来用于新用户的注册及老用户的登录校验,相信每个人都体验过,当你在一个网站上注册一个用户时,应该经常会遇到“您的用户名已被使用”,或者在登录的时候输错密码了网页会提示你“您输入的用户名或密码有误”等等类似情况,这种功能是服务器端通过用户键入的用户名密码和数据库中已记录下来的用户名密码数据进行校验实现的。若每次用户请求我们都需要新建一个数据库连接,请求结束后我们释放该数据库连接,当用户请求连接过多时,这种做法过于低效,所以类似线程池的做法,我们构建一个数据库连接池,预先生成一些数据库连接放在那里供用户请求使用。
(找不到mysql/mysql.h头文件的时候,需要安装一个库文件:sudo apt install libmysqlclient-dev)
我们首先看单个数据库连接是如何生成的:

  1. 使用mysql_init()初始化连接
  2. 使用mysql_real_connect()建立一个到mysql数据库的连接
  3. 使用mysql_query()执行查询语句
  4. 使用result = mysql_store_result(mysql)获取结果集
  5. 使用mysql_num_fields(result)获取查询的列数,mysql_num_rows(result)获取结果集的行数
  6. 通过mysql_fetch_row(result)不断获取下一行,然后循环输出
  7. 使用mysql_free_result(result)释放结果集所占内存
  8. 使用mysql_close(conn)关闭连接

对于一个数据库连接池来讲,就是预先生成多个这样的数据库连接,然后放在一个链表中,同时维护最大连接数MAX_CONN,当前可用连接数FREE_CONN和当前已用连接数CUR_CONN这三个变量。同样注意在对连接池操作时(获取,释放),要用到锁机制,因为它被所有线程共享。

13. HTTP状态码

HTTP返回状态码是HTTP协议在客户端请求后,服务器返回的一个数字代码,用于表示请求的处理结果。状态码分为5类,每类有一定的含义。以下是一些常见的HTTP返回状态码及其含义:

  1. 1xx - Informational Responses: 信息响应

    • 100 Continue:服务器收到请求,需要客户端继续发送请求。
    • 101 Switching Protocols:服务器已经理解请求,正在切换协议。
  2. 2xx - Successful Responses: 成功响应

    • 200 OK:请求成功。
    • 201 Created:请求已经被实现,资源被新建。
    • 204 No Content:服务器成功处理请求,但没有返回任何内容。
  3. 3xx - Redirection Responses: 重定向响应

    • 301 Moved Permanently:资源的URI已被更新,永久性重定向。
    • 302 Found:资源的URI临时变更,临时性重定向。
    • 304 Not Modified:客户端缓存的资源无需重新传输。
  4. 4xx - Client Error Responses: 客户端错误响应

    • 400 Bad Request:请求语法错误。
    • 401 Unauthorized:请求需要用户验证。
    • 403 Forbidden:服务器拒绝请求。
    • 404 Not Found:请求资源不存在。
  5. 5xx - Server Error Responses: 服务器错误响应

    • 500 Internal Server Error:服务器内部错误。
    • 501 Not Implemented:服务器不支持请求的功能。
    • 503 Service Unavailable:服务器暂时无法处理请求。

这只是一些常见的HTTP状态码,HTTP协议定义了更多的状态码来表示各种情况。状态码的使用可以帮助客户端和服务器了解请求的处理结果,并根据不同的状态码采取相应的处理措施。

14.生成HTTP响应并返回给用户

通过以上操作,我们已经对读到的请求做好了处理,然后也对目标文件的属性作了分析,若目标文件存在、对所有用户可读且不是目录时,则使用mmap将其映射到内存地址m_file_address处,并告诉调用者获取文件成功FILE_REQUEST。 接下来要做的就是根据读取结果对用户做出响应了,也就是到了process_write(read_ret);这一步,该函数根据process_read()的返回结果来判断应该返回给用户什么响应,我们最常见的就是404错误了,说明客户请求的文件不存在,除此之外还有其他类型的请求出错的响应,具体的可以去百度。然后呢,假设用户请求的文件存在,而且已经被mmapm_file_address这里了,那么我们就将做如下写操作,将响应写到这个connfd的写缓存m_write_buf中去:

case FILE_REQUEST: {
    add_status_line(200, ok_200_title);
    if(m_file_stat.st_size != 0) {
        add_headers(m_file_stat.st_size);
        m_iv[0].iov_base = m_write_buf;
        m_iv[0].iov_len = m_write_idx;
        m_iv[1].iov_base = m_file_address;
        m_iv[1].iov_len = m_file_stat.st_size;
        m_iv_count = 2;
        bytes_to_send = m_write_idx + m_file_stat.st_size;
        return true;
    }
    else {
        const char* ok_string = "<html><body></body></html>";
        add_headers(strlen(ok_string));
        if(!add_content(ok_string))
            return false;
    }
}

首先将状态行写入写缓存,响应头也是要写进connfd的写缓存(HTTP类自己定义的,与socket无关)中的,对于请求的文件,我们已经直接将其映射到m_file_address里面,然后将该connfd文件描述符上修改为EPOLLOUT(可写)事件,然后epoll_Wait监测到这一事件后,使用writev来将响应信息和请求文件聚集写TCP Socket本身定义的发送缓冲区(这个缓冲区大小一般是默认的,但我们也可以通过setsockopt来修改)中,交由内核发送给用户。

15. linux中使用source 的功能是什么

在Linux中,source 命令用于在当前shell环境中执行指定的脚本文件,从而将脚本中的命令逐行地应用于当前shell会话。这也被称为 “sourcing” 脚本。

具体来说,source 命令的功能包括:

  1. 加载环境变量和配置文件:通过在当前shell环境中执行特定的脚本文件,可以设置环境变量、别名、函数以及其他配置项,以便它们在当前会话中生效。

  2. 更新当前shell状态:当你执行脚本文件时,其中的命令会直接影响当前shell环境。这意味着你可以通过脚本来修改当前会话的状态,而不需要在新的子shell中运行脚本。

  3. 避免创建子shell:与使用 ./script.sh 运行脚本不同,使用 source script.sh 不会创建新的子shell。脚本中的变更将直接在当前shell环境中生效。

例如,假设你有一个名为 myscript.sh 的脚本文件,其中包含了设置环境变量的命令。如果你直接运行 ./myscript.sh,那么变量的设置只会影响子shell,而不会影响当前shell。但是,如果你使用 source myscript.sh. myscript.sh,那么脚本中的环境变量设置将会影响到当前shell环境。

总之,source 命令允许你在当前shell环境中执行脚本,从而影响环境变量、配置项和状态,而无需创建新的子shell。

16. kmalloc和vmalloc函数的区别

kmallocvmalloc 都是在Linux内核中用于动态分配内存的函数,但它们有不同的特点和用途。以下是它们的主要区别:

  1. 分配的内存区域

    • kmalloc:用于分配较小的、固定大小的内存块。分配的内存通常在一页大小(通常为4KB)以内,适用于分配较小的数据结构和对象。
    • vmalloc:用于分配较大的、可变大小的内存块。分配的内存可以超过一页大小,适用于分配较大的缓冲区、数据结构或用于I/O操作的内存。
  2. 内存来源

    • kmalloc:从内核的物理内存池中分配内存。因此,分配的内存在物理上是连续的,适合于对性能有要求的场景。
    • vmalloc:从虚拟内存区域中分配内存,这些虚拟内存区域可能不是物理上连续的。这使得vmalloc 可以分配更大的内存块,但在性能上可能会有一些开销。
  3. 可用内存大小

    • kmalloc:受限于系统可用的物理内存大小和页大小,因此一次分配的内存通常不会太大。
    • vmalloc:由于分配的内存来自虚拟内存区域,因此可以分配较大的内存块,但在页表等方面可能会产生一些开销。
  4. 分配和释放开销

    • kmalloc:通常比 vmalloc 更高效,因为它分配的内存块是连续的,没有额外的页表等开销。
    • vmalloc:由于分配的内存可能不是连续的,因此在分配和释放时可能会产生更多的开销。

总之,kmallocvmalloc 都有自己的适用场景。如果需要分配较小、固定大小的内存块,可以使用 kmalloc。如果需要分配较大、可变大小的内存块,或者需要分配超过一页大小的内存,可以考虑使用 vmalloc

17.linux查看内存使用情况指令

在Linux系统中,你可以使用不同的命令来查看内存使用情况。以下是一些常用的命令:

  1. free 命令:用于查看系统的内存和交换空间使用情况。

    使用示例:

    free -h

    这将以人类可读的格式显示内存和交换空间的使用情况。

  2. top 命令:交互式的实时监视系统的资源使用情况,包括内存、CPU、进程等。

    使用示例:

    top

    top 命令界面中,按下 Shift+M 可以按照内存使用情况排序显示进程。

  3. htop 命令:类似于 top,但提供了更多功能和可视化选项。

    使用示例:

    htop

    htop 命令提供了更直观的界面和更多交互功能。

  4. vmstat 命令:显示虚拟内存统计信息,包括内存、交换、I/O、系统等。

    使用示例:

    vmstat

    默认情况下,vmstat 显示的是系统的整体统计信息。你也可以使用 vmstat -s 查看更详细的信息。

  5. cat /proc/meminfo 命令:读取 /proc/meminfo 文件,该文件包含了有关系统内存使用情况的详细信息。

    使用示例:

    cat /proc/meminfo

    这将显示内存使用的详细信息,包括可用内存、缓存、交换等。

这些命令可以帮助你查看Linux系统中的内存使用情况,你可以根据需要选择合适的命令来获取所需的信息。

18.linux驱动相关指令

insmod:加载驱动模块

rmmod:移除驱动模块

lsmod:查看当前系统中的驱动模块

cat /proc/devices:查看系统当前驱动模块的设备号

19.ps aux

ps aux 是一个常用的Linux命令,用于显示当前系统上所有运行的进程的信息。该命令可以用于查看每个进程的详细信息,包括进程的状态、资源使用情况等。

具体而言,ps aux 命令会显示以下列信息:

  • USER:进程的所有者。
  • PID:进程的ID。
  • %CPU:进程当前使用的CPU占比。
  • %MEM:进程当前使用的内存占比。
  • VSZ:进程的虚拟内存大小。
  • RSS:进程的物理内存大小(Resident Set Size)。
  • TTY:终端类型,指示进程连接到哪个终端。
  • STAT:进程状态,例如 R(运行)、S(睡眠)、Z(僵尸)等。
  • START:进程启动的时间。
  • TIME:进程累计的CPU时间。
  • COMMAND:进程的命令行。

使用示例:

ps aux

该命令会列出所有当前正在运行的进程的信息,用于帮助你监视系统上的活动和资源使用情况。如果需要更详细的信息或者想要指定特定的进程显示,请查阅 ps 命令的帮助文档。

20. mknod /dev/test c 237 0

mknod /dev/test c 237 0 是在Linux系统中创建一个字符设备文件 /dev/test 的命令。它使用了 mknod 命令来创建设备文件,其中的参数含义如下:

  • /dev/test:指定要创建的设备文件的路径和名称。
  • c:表示创建的是一个字符设备文件。
  • 237:主设备号(Major Device Number)。主设备号用于标识设备的类型,这个数字通常与特定设备驱动程序相关联。
  • 0:次设备号(Minor Device Number)。次设备号在同一类型的设备中用于区分不同的设备实例。

创建设备文件后,用户空间程序可以通过打开 /dev/test 来与该字符设备进行交互,使用相应的读、写、控制等操作。

需要注意的是,使用 mknod 命令创建设备文件需要具有足够的权限(通常需要管理员权限)。此外,现代Linux系统中,通常使用 udev 等工具来自动管理设备文件的创建和删除,因此手动使用 mknod 创建设备文件的情况较少。

21. man

在Linux系统中,man 命令用于查看系统文档的手册页(manual pages)。手册页被分为多个节(sections),每个节包含不同类型的信息。man 命令后面的数字参数用于指定要查看的手册节。

以下是常见的一些手册节及其功能:

  • man 1 手册节

    • man 1 通常包含用户命令(User Commands)的文档。这些是终端用户可以在命令行中运行的命令,例如常用的 Linux 命令。
    • 这些文档描述了命令的用法、选项、参数以及示例。
  • man 2 手册节

    • man 2 通常包含系统调用(System Calls)的文档。系统调用是操作系统提供的接口,用于执行底层操作,如文件操作、进程管理等。
    • 这些文档描述了系统调用的用法、参数、返回值等。
  • man 3 手册节

    • man 3 通常包含库函数(Library Functions)的文档。库函数是预编译的函数,用于执行常见的任务,如字符串处理、数学运算、时间处理等。
    • 这些文档描述了库函数的用法、参数、返回值等。
  • man 4 手册节

    • man 4 通常包含特殊文件和设备的文档。这些文件和设备包括特殊设备文件、配置文件等。
    • 这些文档描述了文件和设备的用法、配置、属性等。

使用示例:

  • man 1 ls:查看 ls 命令的帮助文档。
  • man 2 open:查看 open 系统调用的帮助文档。
  • man 3 printf:查看 printf 库函数的帮助文档。
  • man 4 passwd:查看 passwd 配置文件的帮助文档。

你可以根据需要使用不同的手册节来查看命令、系统调用、库函数和文件的帮助文档。

22. Platform总线

https://blog.csdn.net/daocaokafei/article/details/113063481

​ 当有设备的硬件信息注册到platform_bus_type 总线的时候,会遍历所有platform总线维护的驱动,
通过名字来匹配,如果相同,就说明硬件信息和驱动匹配,就会调用驱动的platform_driver ->probe函数,初始化驱动的所有资源,让该驱动生效。

​ 当有设备的驱动注册到platform_bus_type 总线的时候,会遍历所有platform总线维护的硬件信息,
通过名字来匹配,如果相同,就说明硬件信息和驱动匹配,就会调用驱动的platform_driver ->probe函数,初始化驱动的所有资源,让该驱动生效。

23. Linux内核中如何访问另外一个模块的函数和变量

该问题其实是模块符号导出问题,实现该功能比较简单,借助EXPORT_SYMBOL()即可。

这里的符号主要指的是全局变量和函数,静态全局变量其实也可以被另外一个模块访问到。

Linux内核采用的是以模块化形式管理内核代码。内核中的每个模块相互之间是相互独立的,也就是说A模块的全局变量和函数,B模块是无法直接访问的。

符号导出了,也就是说你可以把你实现的函数接口和全局变量导出,以供其他模块使用。

在Linux内核的世界里,如果一个模块已经以静态的方式编译进的内核,那么它导出的符号就会出现在全局的内核符号表中。

在Ubuntu 14.04系统中,Linux内核的全局符号表存放在以下文件:

/usr/src/linux-headers-3.2.0-29-generic-pae/Module.symvers

如何导出符号?

Linux内核给我们提供了两个宏:

EXPORT_SYMBOL(name);
EXPORT_SYMBOL_GPL(name);

上面宏定义的任一个使得给定的符号在模块外可用;
GPL版本的宏定义只能使符号对GPL许可的模块可用;
符号必须在模块文件的全局部分输出,在任何函数之外,因为宏定义扩展成一个特殊用途的并被期望是全局存取的变量的声明。

模块编译时,如何寻找使用的符号?

  • a.在本模块中符号表中,寻找符号(函数或变量实现)
  • b.在内核全局符号表中寻找
  • c.在模块目录下的Module.symvers文件中寻找

案例演示

模块A导出全局变量global_var和函数show两个符号供模块B使用。

A模块

#include <linux/init.h>
#include <linux/module.h>
static int global_var = 100;
static void show(void)
{
	printk("show():  global_var =%d \n",global_var);
}
static int hello_init(void)
{
	printk("module b :global_var=%d\n",global_var);
	return 0;
}
static void hello_exit(void)
{
	printk("hello_exit \n");
	return;
}EXPORT_SYMBOL(global_var);
EXPORT_SYMBOL(show);
MODULE_AUTHOR("yikoulinux");
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);	

B模块

#include <linux/init.h>
#include <linux/module.h>

extern int global_var;
extern  void show(void);
static int hello_init(void)
{
	printk("module a: global_var= %d\n",global_var);
	show();
	return 0;
}
static void hello_exit(void)
{
	printk("hello_exit \n");
	return;
}
MODULE_AUTHOR("yikoulinux");
MODULE_LICENSE("GPL");
module_init(hello_init);
module_exit(hello_exit);

调试步骤:

1.编译模块A,然后加载模块A,在模块A编译好后,在它的当前目录会看到一个Module.symvers文件,这里存放的就是我们模块A导出的符号。
2.将模块A编译生成的Module.symvers文件拷贝到模块B目录下,然后编译模块B,加载模块B。
3.通过dmesg查看模块打印的信息。

由结果可知,我们在B模块中访问到了模块A的全局变量global_var以及函数show。

24. 设备树

1.1.设备树感性认识

设备树(Device Tree),将这个词分开就是“设备”和“树”,描述设备树的文件叫做DTS(Device Tree Source),这个DTS 文件采用树形结构描述板级设备,比如CPU 数量内存基地址IIC 接口上接了哪些设备、SPI 接口上接了哪些设备等等。
设备树是树形数据结构,具有描述系统中设备的节点。每个节点都有描述所代表设备特征的键值对。每个节点只有一个父节点,而根节点则没有父节点。

1.2.DTS、DTB、DTC

DTS:设备树源码文件;
DTB:将DTS编译后得到的二进制文件;
DTC:DTS的编译工具,其源码在内核的scripts\dtc目录下。
基于同样arm架构的CPU有很多,同一个CPU会制作很多配置不一的板子,如何正确的编译所选的板子的DTS文件呢?在内核的arch/arm/boot/dts/Makefile中:

dtb-$(CONFIG_ARCH_XXX) += xxx.dtb
dtb-$(CONFIG_ARCH_XXX) += xxx-sip.dtb
dtb-$(CONFIG_ARCH_XXX) += xxx.dtb
dtb-$(CONFIG_ARCH_XXX) += xxx.dtb

例如xxxx的开发板,只要设置CONFIG_ARCH_xxx=y,所有用到这颗SOC的DTS都会编译成DTB。如果后续还用到了这颗SOC设计的开发板,只要新建一个DTS文件,并将对应名称的DTB文件名加到dtb-$(CONFIG_ARCH_xxx)中,在编译设备树时就会将DTS编译为二进制的DTB文件。

1.3.2. 设备节点

设备树中的每一个节点都按照以下格式命名:

node-name@unit-address

1.3.4. 基本设备节点类型

所有设备树文件均要包含一个根文件,并且所有设备树文件均应在根节点下存在以下节点:

  • 1个/cpus节点
  • 至少一个/memory节点

使用说明:R = 必需,O = 可选,OR = 可选但推荐,SD = 参见定义,所有其他的标准属性均可接受,但可选

25.设备树解析流程

内核启动并获取设备树

在uboot引导内核的时候,会将设备树在物理内存中的物理起始内存地址传递给Linux内核,然后Linux内核在unflattern_device_tree中解析设备镜像,并利用扫描到的信息创建由device node构成的链表,全局变量of_allnodes指向链表的根节点,设备树的每一个节点都由一个struct device_node与之对应。
unflatten_device_tree的意思是解开设备树,在这个函数里调用了__unflatten_device_tree这一函数:

2.2.创建platform_device

内核从启动到创建设备的过程大致如下:
在do_initcalls中会传递level给do_initcall_level来调用不同层次的初始化函数,level的对应关系见linux-3.10/include/linux/init.h 第196行。
在这个初始化过程中,会调用一个customize_machine的函数。

2.3.Platform driver注册流程

此节分析Platform driver的注册流程,以memctrl驱动的注册为例分析。关于系统调用驱动初始化函数的流程分析,参考自动初始化机制章节。本章节分析从设备驱动文件的xxx_init函数开始分析。

26. 设备树-使用设备资源

27. 设备树-自动初始化机制

4.1.编译到内核

4.1.1. module_init宏展开

Linux中每一个模块都有一个module_init函数,并且有且只有一个。

4.1.2. 链接脚本

在linux3.10/arch/arm/kernel/vmlinux.lds.S中:

4.2.动态加载的模块(.ko)

4.2.1. Module_init展开

4.2.2. *mod.c文件

编译成module的模块都会自动产生一个*.mod.c的文件

4.2.3. 动态加载

insmod是busybox提供的用户层命令:
路径busybox/modutils/ insmod.c

28. 安装交叉编译工具

1.下载交叉编译器

本文使用Linaro出品的交叉编译器, Linaro是一间非营利开放源代码软件工程公司,最著名的就是Linaro GCC编译工具链(编译器),其官网可以下载源码。Linaro 网站提供了多种GCC交叉编译工具链,我们使用的是Cortex-A7内核的开发板,因此选择arm-linux-gnueabihf,再根据32/64位系统下载不同版本的编译器,这里选择下载x86_64版本 。

⏩ 拷贝完成后在该目录中对交叉编译工具进行解压

sudo tar -vxf gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf.tar.xz

⏩ 打开/etc/profile文件,修改环境变量,在文件末尾添加如下内容

export PATH=$PATH:/usr/local/arm/gcc-linaro-4.9.4-2017.01-x86_64_arm-linux-gnueabihf

⏩ 使用交叉编译器,还需要安装如下库

sudo apt-get install lsb-core lib32stdc++6

使用以下命令查看交叉编译工具的版本号,若安装正确就会显示版本号

arm-linux-gnueabihf-gcc -v

29、简单描述linux设备驱动中的总线,设备和驱动的关系。

platform总线将设备和驱动绑定。

在系统每注册一个设备的时候,会寻找与之匹配的驱动;

相反的,在系统每 注册一个驱动的时候,会寻找与之匹配的设备,而匹配由总线完成。

一个现实的Linux设备和驱动通常都需要挂接在一种总线上。

设备与驱动的关联通过总线的match()方法进行匹配,驱动挂载总线时与所有设备进行匹配,设备挂载总线时与所有的驱动进行匹配,所以驱动和设备的挂载无先后之分。

匹配成功后会通过调用驱动的probe()方法来初始化设备。

30.内核解析设备树一般过程

系统启动后,uboot会从网络或者flash、sd卡中读取设备树文件(具体由uboot命令给出),

引导linux内核启动后,会把设备树镜像保存到的内存地址传递给Linux内核,Linux内核会解析设备树镜像,从设备树中提取硬件信息并逐一初始化。

其中设备树信息会被转换成struct platform_device类型变量。

而驱动要解析设备树,必须定义 struct platform_driver类型结构体变量,并通过函数platform_driver_register()注册。

这两者都会注册到platform总线,当驱动和设备树节点匹配成功后,就调用 struct platform_driver中.probe方法。

其中设备树节点会封装在struct device_node结构体变量中
各个属性信息会封装在 struct property结构体变量中,
他们与struct platform_device结构体之间关系如下:

31.字符设备

一、字符设备架构

下面我们以两个设备:LED、MPU6050为例来讲解字符设备的架构

img

由上图所示:

1、硬件

外设有MPU6050、LED两个设备,他们通过外设电路连接到SOC的对应的引脚上。
程序要操作外设,就要通过设置soc中对应的SFR来与外设交互。

2、驱动层

每一个字符设备都必须首先定义一个结构体变量struct cdev,并注册到内核中
所有的该变量在内核中会通过链表进程管理,其中成员list用于将所有链表串接起来
用于操作外设的功能函数全部被封装在struct file_operations中,包括read、write等
每一个字符设备都必须要有一个设备号,保存在成员dev中,
主、次设备号只能被分配一次
所有的字符设备号,都由数组chrdevs统一管理
chrdevs是一个指针数组,成员类型为**struct char_device_struct ***,下标与字符设备号有一定的对应关系,
**struct char_device_struct **中有成员:

unsigned int major;
struct cdev *cdev; 

major : 是主设备号
cdev : 指向该字符设备号对应的cdev结构体

3、应用层、VFS层

  • 用户如果想操作硬件,必须调用内核中的struct file_operations中的操作函数,
  • 那么如何才能找到该结构体呢?
    必须要依赖文件节点来查找,可以通过以下命令来创建
mknod  /dev/led c 250 0
    mknod 创建设备文件,可以使字符设备,也可以是块设备
    /dev/led 设备文件名
    c  字符设备
    250  主设备号
    0    次设备号

字符设备文件属性中最重要的属性就是字符设备号,该设备号和chedevs的下标有一定对应关系

通过mknod创建的文件,VFS层会分配一个结构体变量来维护该文件,类型为struct inode

每新建1个文件内核都会创建不同的结构体变量与之对应

应用程序要操作某个字符设备,那么必须先通过系统调用open()来打开该字符设备

该函数会返回一个唯一的整型文件描述符,同时内核中会分配结构体变量,类型为struct file,并与文件描述符一一对应,该结构体维护在struct task_struct

每次打开某个文件,都会分配不同的文件描述符,所以需要用不同的变量来保存文件描述符

二、字符设备驱动创建的流程

了解了架构之后,那么我们来看一下内核中完整的创建字符设备的流程及对应的函数调用关系:

如下图所示,字符设备的创建主要包括以下三个步骤:

  1. 申请设备号
  2. 初始化cdev
  3. 注册cdev
    调用的函数见右侧

img

下面是一个最简单的字符设备创建的实例

/*  
 *一口Linux
 *2021.6.21
 *version: 1.0.0
*/

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kdev_t.h>
#include <linux/fs.h>
#include <linux/cdev.h>

static int major = 237;
static int minor = 0;
static dev_t devno;
static struct cdev cdev;
static int hello_open (struct inode *inode, struct file *filep)
{
	printk("hello_open()\n");
	return 0;
}
static struct file_operations hello_ops = 
{
	.open = hello_open,
};
static int hello_init(void)
{
	int result;
	int error;	
	printk("hello_init \n");
	devno = MKDEV(major,minor);	
	result = register_chrdev_region(devno, 1, "test");
	if(result<0)
	{
		printk("register_chrdev_region fail \n");
		return result;
	}
	cdev_init(&cdev,&hello_ops);
	error = cdev_add(&cdev,devno,1);
	if(error < 0)
	{
		printk("cdev_add fail \n");
		unregister_chrdev_region(devno,1);
		return error;
	}
	return 0;
}
static void hello_exit(void)
{
	printk("hello_exit \n");
	cdev_del(cdev);
	unregister_chrdev_region(devno,1);
	return;
}
module_init(hello_init);
module_exit(hello_exit);

该实例代码主要功能:

  1. 申请了字符设备号237
  2. 初始化cdev,并注册了cdev

应用程序如果要想使用,还必须创建字符设备节点

mknod /dev/test c 237 0

这样应用程序就可以通过设备节点/dev/test 调用到对应的内核操作函数.open = hello_open,

/*  
 *一口Linux
 *2021.6.21
 *version: 1.0.0
*/

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
main()
{
	int fd;
	fd = open("/dev/test",O_RDWR);
	if(fd<0)
	{
		perror("open fail \n");
		return;
	}
	printf("open ok \n ");
}

32. 字符设备架构、inode、cdev、file_operations、file之间关系;

在Linux内核中,字符设备(Character Device)是一种设备类型,它以字符为单位进行数据的输入和输出。在字符设备的架构中,涉及到以下几个重要的概念:inode、cdev、file_operations 和 file。这些概念之间的关系如下:

  1. inode(索引节点):

    • 在Linux文件系统中,每个文件和目录都有一个对应的inode,它存储了文件的元数据,如文件类型、权限、所有者等。
    • 对于字符设备来说,inode 中的 i_mode 字段会标识文件类型为字符设备。
    • inode 中的 i_rdev 字段会保存字符设备的主设备号和次设备号。
  2. cdev(字符设备结构体):

    • cdev 是内核中用于表示字符设备的结构体,它包含了字符设备的操作和属性。
    • cdev 结构体通过 cdev_init 函数进行初始化,并与对应的字符设备驱动关联。
    • 每个字符设备都需要一个 cdev 结构体。
  3. file_operations(文件操作函数集):

    • file_operations 结构体定义了一组操作函数,用于操作字符设备文件。
    • 这些操作函数包括打开、关闭、读取、写入等操作。
    • 在字符设备驱动中,需要实现并填充 file_operations 结构体。
  4. file(文件结构体):

    • file 结构体表示打开的文件实例,它与特定的字符设备文件关联。
    • file 结构体包含了有关文件的信息,如文件位置、打开模式等。
    • 当用户打开一个字符设备文件时,会创建一个对应的 file 结构体。

关系解释:

  • 当用户打开一个字符设备文件时,内核会创建一个 file 结构体,与该文件相关联。
  • file 结构体包含一个指向 inode 的指针,从而知道该文件的元数据信息。
  • cdev 结构体通常在字符设备驱动中定义并初始化,包含了对应字符设备的操作函数集。
  • 在字符设备驱动中,将 cdev 结构体与 file_operations 结构体关联,使驱动能够处理字符设备操作。
  • 用户操作字符设备文件时,操作函数将通过相应的 file 结构体和 cdev 结构体来执行。

33. Linux 应用程序与驱动程序的关系

Linux 应用程序调用驱动程序的步骤大致如下:

image-20230827220052269

  1. 应用程序调用库凼数提供的 open()函数打开某个设备文件;
  2. 库根据 open()凼数的输入参数引起 CPU 异常,进入内核;
  3. 内核的异常处理凼数根据输入参数找到相应的驱动程序,返回文件句柄给库,库函数再返回给应用程序;
  4. 应用程序再使用得到的文件句柄调用 write()、read()等函数发出控制指令;
  5. 库根据 write()、read()等凼数的输入参数引起 CPU 异常,迕入内核;
  6. 内核的异常处理函数数根据输入参数调用相应的驱动程序执行相应的操作。

34 .内核空间和用户空间

对 32 位操作系统而言,它的寻址空间(虚拟地址空间,或叫线性地址空间)为 4G(2的32次方)。也就是说一个进程的最大地址空间为 4G。操作系统的核心是**内核(kernel)**,它独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证内核的安全,现在的操作系统一般都强制用户进程不能直接操作内核。具体的实现方式基本都是由操作系统将虚拟地址空间划分为两部分,一部分为内核空间,另一部分为用户空间。针对 Linux 操作系统而言,最高的 1G 字节(从虚拟地址 0xC0000000 到 0xFFFFFFFF)由内核使用,称为内核空间。而较低的 3G 字节(从虚拟地址 0x00000000 到 0xBFFFFFFF)由各个进程使用,称为用户空间。
对上面这段内容我们可以这样理解:
每个进程的 4G 地址空间中,最高 1G 都是一样的,即内核空间。只有剩余的 3G 才归进程自己使用。
换句话说就是, 最高 1G 的内核空间是被所有进程共享的!
下图描述了每个进程 4G 地址空间的分配情况(此图来自互联网):

img

为什么需要区分内核空间与用户空间

在 CPU 的所有指令中,有些指令是非常危险的,如果错用,将导致系统崩溃,比如清内存、设置时钟等。如果允许所有的程序都可以使用这些指令,那么系统崩溃的概率将大大增加。
所以,CPU 将指令分为特权指令和非特权指令,对于那些危险的指令,只允许操作系统及其相关模块使用,普通应用程序只能使用那些不会造成灾难的指令。比如 Intel 的 CPU 将特权等级分为 4 个级别:Ring0~Ring3。
其实 Linux 系统只使用了 Ring0 和 Ring3 两个运行级别(Windows 系统也是一样的)。当进程运行在 Ring3 级别时被称为运行在用户态,而运行在 Ring0 级别时被称为运行在内核态。

内核态与用户态

好了我们现在需要再解释一下什么是内核态、用户态:
当进程运行在内核空间时就处于内核态,而进程运行在用户空间时则处于用户态。
在内核态下,进程运行在内核地址空间中,此时 CPU 可以执行任何指令。运行的代码也不受任何的限制,可以自由地访问任何有效地址,也可以直接进行端口的访问。
在用户态下,进程运行在用户地址空间中,被执行的代码要受到 CPU 的诸多检查,它们只能访问映射其地址空间的页表项中规定的在用户态下可访问页面的虚拟地址,且只能对任务状态段(TSS)中 I/O 许可位图(I/O Permission Bitmap)中规定的可访问端口进行直接访问。

对于以前的 DOS 操作系统来说,是没有内核空间、用户空间以及内核态、用户态这些概念的。可以认为所有的代码都是运行在内核态的,因而用户编写的应用程序代码可以很容易的让操作系统崩溃掉。
对于 Linux 来说,通过区分内核空间和用户空间的设计,隔离了操作系统代码(操作系统的代码要比应用程序的代码健壮很多)与应用程序代码。即便是单个应用程序出现错误也不会影响到操作系统的稳定性,这样其它的程序还可以正常的运行(Linux 可是个多任务系统啊!)。

所以,区分内核空间和用户空间本质上是要提高操作系统的稳定性及可用性。

35 .如何从用户空间进入内核空间

其实所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件,分配回收内存,从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。
比如应用程序要读取磁盘上的一个文件,它可以向内核发起一个 “系统调用“ 告诉内核:”我要读取磁盘上的某某文件”。其实就是通过一个特殊的指令让进程从用户态进入到内核态(到了内核空间),在内核空间中,CPU 可以执行任何的指令,当然也包括从磁盘上读取数据。具体过程是先把数据读取到内核空间中,然后再把数据拷贝到用户空间并从内核态切换到用户态。此时应用程序已经从系统调用中返回并且拿到了想要的数据,可以开开心心的往下执行了。
简单说就是应用程序把高科技的事情(从磁盘读取文件)外包给了系统内核,系统内核做这些事情既专业又高效。

对于一个进程来讲,从用户空间进入内核空间并最终返回到用户空间,这个过程是十分复杂的。举个例子,比如我们经常接触的概念 “堆栈”,其实进程在内核态和用户态各有一个堆栈。运行在用户空间时进程使用的是用户空间中的堆栈,而运行在内核空间时,进程使用的是内核空间中的堆栈。所以说,Linux 中每个进程有两个栈,分别用于用户态和内核态。

下图简明的描述了用户态与内核态之间的转换:

img

既然用户态的进程必须切换成内核态才能使用系统的资源,那么我们接下来就看看进程一共有多少种方式可以从用户态进入到内核态。概括的说,有三种方式:系统调用、软中断和硬件中断。这三种方式每一种都涉及到大量的操作系统知识,所以这里不做展开。

36.字符设备的几种创建注销方法

1. register_chdev

int register_chrdev(unsigned int major, const char *name, const struct file_operations*fops);
void unregister_chrdev(unsigned int major, const char *name);

每个设备文件都有主次设备号 major,主设备号是唯一的,每个主设备下有次设备号 minor,次设备号在每个主设备下也是唯一的。在 Linux 系统下输入 cat /proc/devices 命令可以查看已被注册的主设备号。

缺点:

设备号直接在驱动代码中写死了。

  1. 编译驱动代码前,必须要先查看目标系统中设备号的占用情冴;
  2. 更换设备后原先驱动中写死的设备号就可能已被占用;
  3. 原先的驱动注册函数 register_chrdev()输入参数中仅有主设备号而没有次设备号,这意味着一个设备就会占用所有的次设备号,十分浪费。

2. register_chrdev_region

  1. 当驱劢程序需要给定主讴备号时,使用凼数来注册讴备号:
int register_chrdev_region(dev_t from, unsigned count, const char *name);
int major = 200;
//主设备号指定为 200
int minor = 0;
//次设备号为 0
dev_t devid = MKDEV(major, minor);
//通过主次设备号获得设备号
/* 向内核注册设备号 */
register_chrdev_region(devid, 1, "xxx-dev");
  1. 驱动程序不需要指定主设备号时,使用凼数:
int alloc_chrdev_region(dev_t *dev, unsigned baseminor, unsigned count, const char *name)

image-20230828100700477

注销设备号需要使用同一个函数:

void unregister_chrdev_region(dev_t from, unsigned count);

3. cdev_add

struct cdev {
struct kobject kobj;
struct module *owner;
const struct file_operations *ops;
struct list_head list;
dev_t dev;
unsigned int count;
};

重要成员变量:
owner:一般设置为 THIS_MODULE;
ops:设备操作函数指针;
dev:设备号。

image-20230828101102912

image-20230828101232926

image-20230828101301912

/* 字符设备 */
struct cdev ax_cdev = {
.owner = THIS_MODULE,
};
/* 设备操作函数 */
static struct file_operations ax_fops = {
.owner = THIS_MODULE,
.open……
};
/* 设备号 */
cdev_t devid;
/* 申请设备号 */
alloc_chrdev_region(&devid, 0, 1, " xxx-dev");
/* ax_cdev 变量初始化 */
cdev_init(&ax_cdev, &ax_fops);
/* 注册字符设备 */
cdev_add(&ax_cdev, devid, 1);
……
/* 此处有省略 */
/* 卸载字符设备 */
cdev_del(&ax_cdev);

37.自动创建设备文件

mdev

medv 是一个用户程序,是 udev 的简化版。它可以检测并根据系统中硬件设备状态来创建或者删除设备文件。在加载驱动模块后,会自动在/dev 目录下创建设备节点文件,卸载驱动模块后设备节点也会自动删除。接下来看看如何实现。

类的创建和删除

创建设备前需要先创建类,设备是在类下面创建的。需要使用class_create()来创建,class_create是个宏定义:

#define class_create(owner, name) \
({ \
static struct lock_class_key __key; \
__class_create(owner, name, &__key); \
})
struct class *__class_create(struct module *owner, const char *name, struct lock_class_key *key)

卸载驱动程序时需要删除类,使用函数 class_destroy(),原型如下:

void class_destroy(struct class *cls);

设备节点的创建和删除:

创建类后,使用 device_create()凼数在类下面创建设备,原型为:

image-20230828104645157

自动创建设备节点的实现一般放在驱动入口函数中,结合上一节类的创建以及设备号,自动创建设备节点的实现示例如下:

struct class *class;
struct device *device;
dev_t devid;
/* 类 */
/* 设备 */
/* 设备号 */
/* 驱动入口函数 */
static int __init xxx_init(void)
{
    ……
    /* 申请设备号 */
    alloc_chrdev_region(&devid, 0, 1, "xxx-dev");
    ……
    /* 创建类 */
    class = class_create(THIS_MODULE, "xxx");
    /* 创建设备 */
    device = device_create(class, NULL, devid, NULL, "xxx");
    return 0;
}
/* 驱动出口函数 */
static void __exit led_exit(void)
{
    /* 删除设备 */
    device_destroy(class, devid);
    /* 删除类 */
    class_destroy(class);
    /* 注销字符设备 */
    unregister_chrdev_region(devid, 1);
}
module_init(led_init);
module_exit(led_exit);

38.platform 与设备树

1. 设备树下的platform

image-20230828193054678

image-20230828193159632

alinxled {
compatible = "alinx-led";
pinctrl-names = "default";
pinctrl-0 = <&pinctrl_led_default>;
alinxled-gpios = <&gpio0 0 0>;
};

除了这点要注意之外,设备树的写法就没有什么特别的了。

image-20230828193643392

image-20230828193744778

39. Linux驱动模型

Linux驱动模型是Linux内核用于管理设备驱动程序的系统。它定义了一组规则,让Linux内核与设备驱动程序之间的交互更加简单和有效。它为内核和用户空间的设备驱动程序提供了一个标准的接口

设备驱动模型的三个重要成员是总线,驱动,设备

设备模型使得设备与设备驱动程序分离开来,允许不同类型的设备可以由不同的驱动程序管理。设备模型通过一个基于对象的模型来描述所有的设备及其关系,这些对象可以通过相应的API进行访问和操作。

在加载和卸载设备驱动程序时,设备模型会检查设备树上的设备对象,选择与驱动程序匹配的设备对象,并将设备和驱动程序进行关联。这样,当设备对象与对应的驱动程序匹配时,驱动程序就可以对设备进行访问和操作。

40. 设备树是什么?

设备树用于描述系统中所有的设备及其关系。设备树是一个文本文件,描述了设备之间的连接、属性和配置信息。

41.什么是设备文件?

设备文件是一种用户空间程序与设备驱动程序进行交互的接口。在Linux系统中,每个设备都有一个对应的设备文件。设备文件用于读取和写入设备状态,以及控制设备的行为

UDEV是一个Linux设备管理系统,用于管理设备的动态创建和删除。

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

因为在内核中操作的都是虚拟地址,内核访问不到物理地址,只能通过ioremap映射为虚拟地址 内核才能访问此内存空间

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

设备驱动模型的三个重要成员是总线,驱动,设备。

platfoem总线的匹配规则是:要匹配的设备和驱动都要注册,驱动和设备的匹配规则如下

1.基于设备树风格的匹配

2.匹配ID表(即platform_device设备名是否出现在platform_driver的id表内)

3.匹配platform_device设备名和驱动的名字

4.基于ACPI风格的匹配

设备可以在设备树里注册,也可以通过代码注册设备,匹配成功会去调用驱动程序里的probe函数(probe函数在这个platform_driver结构体中注册)。

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

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

卸载模块时曾出现卸载失败的情形,原因是存在进程正在使用模块,检查代码后发现产生了死锁的问题。

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

由于内核空间和用户空间是不能互相访问的,如果需要访问就必须借助内核函数进行数据读写。copy_to_user():完成内核空间到用户空间的复制,copy_from_user():是完成用户空间到内核空间的复制。一般用于file_operations结构里的read,write,ioctl等内存数据交换作用的函数。当然,如果ioctl没有用到内存数据复制,那么就不会用到这两个函数。

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

  1. 主设备号:主设备号标识设备对应的驱动程序。虽然现代的linux内核允许多个驱动程序共享主设备号,但我们看待的大多数设备仍然按照“一个主设备对应一个驱动程序”的原则组织。
  2. 次设备号:次设备号由内核使用,用于正确确定设备文件所指的设备。依赖于驱动程序的编写方式,我们可以通过次设备号获得一个指向内核设备的直接指针,也可将此设备号当作设备本地数组的索引。

chartest 由驱动程序4管理,该文件所指的设备是64号设备。(感觉类似于串口终端或者字符设备终端)。

47.Linux设备中字符设备与块设备有什么主要的区别?请分别列举一些实际的设备说出它们是属于哪一类设备。

  • 字符设备:字符设备是个能够像字节流(类似文件)一样被访问的设备,由字符设备驱动程序来实现这种特性。字符设备驱动程序通常至少实现open,close,read和write系统调用。字符终端、串口、鼠标、键盘、摄像头、声卡和显卡等就是典型的字符设备。
  • 块设备:和字符设备类似,块设备也是通过/dev目录下的文件系统节点来访问。块设备上能够容纳文件系统,如:u盘,SD卡,磁盘等。

字符设备和块设备的区别仅仅在于内核内部管理数据的方式,也就是内核及驱动程序之间的软件接口,而这些不同对用户来讲是透明的。在内核中,和字符驱动程序相比,块驱动程序具有完全不同的接口。

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

  1. 查看驱动模块中打印信息的命令:dmesg
  2. 查看字符设备信息可以用lsmod 和modprobe,lsmod可以查看模块的依赖关系,modprobe在加载模块时会加载其他依赖的模块。
  3. 显示当前使用的中断号cat /proc/interrupt

49. linux中引入模块机制有什么好处?

首先,模块是预先注册自己以便服务于将来的某个请求,然后他的初始化函数就立即结束。换句话说,模块初始化函数的任务就是为以后调用函数预先作准备。好处:

  1. 应用程序在退出时,可以不管资源的释放或者其他的清除工作,但是模块的退出函数却必须仔细此撤销初始化函数所作的一切。
  2. 该机制有助于缩短模块的开发周期。即:注册和卸载都很灵活方便

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

​ 设备驱动模型三个重要成员是 总线、设备、驱动;

platform总线的匹配规则是:要匹配的设备和驱动都要注册,设备可以在设备树里注册,也可以通过代码注册设备,匹配成功会去调用驱动程序里的probe函数(probe函数在这个platform_driver结构体中注册)。

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

mmap函数实现把一个文件映射到一个内存区域,从而我们可以像读写内存一样读写文件,他比单纯调用read/write也要快上许多。在某些时候我们可以把内存的内容拷贝到一个文件中实现内存备份,当然,也可以把文件的内容映射到内存来恢复某些服务。另外,mmap实现共享内存也是其主要应用之一,mmap系统调用使得进程之间通过映射同一个普通文件实现共享内存

52.U-boot的启动流程

这一过程可以分为两个阶段,各个阶段的功能如下:
第一阶段的功能:

  • 硬件设备初始化;
  • 加载u-boot第二阶段的代码到RAM空间;
  • 设置好栈;
  • 跳转到u-boot第二阶段代码的入口处;

第二阶段的功能:

  • 初始化本阶段使用的硬件设备;
  • 检测系统内存映射;
  • 将Linux内核从Flash读取到RAM中;
  • 为Linux内核设置启动参数;
  • 调用Linux内核。

53. 常见的文件系统格式有哪些

常见的文件系统格式包括:

  1. FAT(File Allocation Table):FAT文件系统是一种简单的文件系统格式,用于存储文件和目录信息。它通常用于移动存储设备(如USB闪存驱动器)和早期的Windows操作系统。

  2. NTFS(New Technology File System):NTFS是由Microsoft开发的高级文件系统格式,用于Windows操作系统。它支持更大的文件和分区大小,以及更多的安全性和恢复功能。

  3. ext2、ext3、ext4:这些是Linux操作系统中常见的文件系统格式。ext2是早期的版本,ext3和ext4引入了更多的功能和性能改进。它们通常用于Linux分发版。

  4. **HFS(Hierarchical File System)和HFS+**:HFS是苹果计算机早期的文件系统格式,而HFS+是其更新版本,用于Mac OS X及以后的操作系统。

  5. APFS(Apple File System):APFS是苹果公司开发的新一代文件系统,用于iOS、macOS和其他苹果操作系统。它具有高级的特性,如快照、加密和高性能。

  6. exFAT(Extended File Allocation Table):exFAT是一种用于移动设备和存储介质的文件系统格式,它支持大文件和大容量存储设备。

  7. ReFS(Resilient File System):ReFS是由Microsoft开发的高级文件系统,旨在提供更高的可靠性和恢复性。它通常用于Windows Server操作系统。

  8. Btrfs(B-tree File System):Btrfs是一种Linux文件系统,具有先进的特性,如快照、压缩、数据镜像和校验和。

  9. ZFS(Zettabyte File System):ZFS是一种高级文件系统,最初由Sun Microsystems开发,现在在各种操作系统上可用。它具有高级的数据管理和恢复功能。

这些是一些常见的文件系统格式,不同的操作系统和用途可能使用不同的文件系统格式。选择文件系统格式通常取决于操作系统、性能需求、数据可靠性和文件大小等因素。

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

注册一个字符设备驱动有两种方法:

  1. void cdev_init(struct cdev *cdev, struct file_operations *fops)

该注册函数可以将cdev结构嵌入到自己的设备特定的结构中。cdev是一个指向结构体cdev的指针,而fops是指向一个类似于file_operations结构(可以是file_operations结构,但不限于该结构)的指针.

  1. int register_chrdev(unsigned int major, const char *name , struct file_operations *fopen);

该注册函数是早期的注册函数,major是设备的主设备号,name是驱动程序的名称,而fops是默认的file_operations结构(这是只限于file_operations结构)。对于register_chrdev的调用将为给定的主设备号注册0-255作为次设备号,并为每个设备建立一个对应的默认cdev结构。

55.了解CPU(x86/ARM/MIPS/RISC-V)体系结构

56. IP组播

​ IP组播(IP Multicast)是一种网络通信模式,用于在IP网络中一次向多个接收者发送数据。它与单播(Unicast)和广播(Broadcast)是不同的通信方式。

​ 在IP组播中,发送者只需要发送一份数据,而多个接收者可以选择加入一个或多个特定的组播组,以接收他们感兴趣的数据。这种方式可以有效减少网络带宽的使用,因为数据只需要一次传输,而不是为每个接收者单独传输一份。

以下是IP组播的关键特点和概念:

  1. 组播组(Multicast Group):多个接收者可以加入一个组播组,这是一个标识特定数据流的组。组播组由IP地址表示,通常位于特定的IP地址范围内。
  2. 组播地址(Multicast Address):组播组的IP地址称为组播地址。它是一个保留的IP地址范围,例如224.0.0.0至239.255.255.255。
  3. IGMP(Internet Group Management Protocol):用于管理主机加入和离开组播组的协议。接收者可以使用IGMP通知路由器他们对哪些组播组感兴趣。
  4. 组播路由器:在支持IP组播的网络中,路由器被配置为处理组播流量的转发。它们负责将组播数据从发送者传递到加入相应组播组的接收者。

IP组播常用于多媒体传输、分布式应用程序通信、网络广播等场景,可以节省带宽并减少网络拥塞。

57. 请描述Linux内核中I/O子系统的基本结构及其作用。

​ I/O子系统包括VFS层、设备驱动及其硬件。VFS层是内核的文件系统接口,它负责管理Linux操作系统的文件系统操作。设备驱动及硬件负责管理设备的读取和写入操作。在内核中,通过设备驱动将硬件接口与调用层进行连接。

58. Linux内核中,如何实现设备驱动程序的自动加载?

​ 在Linux内核中,通过配置udev规则可以实现外设自动加载。udev规则会在符合特定设备属性的设备插入或系统启动时,自动加载与该设备相对应的内核模块。

在Linux内核中,设备驱动程序的自动加载通常通过udev(用户空间设备管理器)来实现。udev是一个用户空间的服务,负责管理设备的插拔和设备节点的创建,它能够监测系统中的设备事件并触发相应的操作,包括自动加载设备驱动程序。

以下是实现设备驱动程序的自动加载的一般步骤:

  1. 编写设备规则文件: 首先,您需要创建一个udev规则文件,通常存储在/etc/udev/rules.d/目录下,以.rules为文件扩展名。这个规则文件包含了设备识别和操作的规则。例如,您可以指定设备的特征(如VID(厂商ID)和PID(产品ID)),以及需要加载的驱动程序等信息。

  2. 重载udev: 一旦您创建了规则文件,您需要重载udev服务,以便它能够应用这些规则。您可以使用以下命令来重载udev:

    sudo udevadm control --reload-rules
  3. 插入设备: 当您插入一个与规则匹配的设备时,udev将检测到设备事件,并根据规则自动加载相关的设备驱动程序。

  4. 查看日志: 如果您想查看设备加载的日志,您可以使用以下命令:

    sudo journalctl -f

这将显示实时的系统日志,您可以在其中查看设备加载事件。

总之,通过创建适当的udev规则文件,您可以实现Linux内核中设备驱动程序的自动加载,这样在插入与规则匹配的设备时,系统将自动加载相关的驱动程序。这种方法有助于简化设备管理并提高系统的可扩展性。

59.在Linux内核中,如何进行系统调用?

​ 在Linux内核中,系统调用是通过INT 80或SYSCALL指令来实现的。这两种指令都允许应用程序进入内核空间,并向内核发出系统调用请求。内核会根据请求执行相应的操作,并将结果返回给应用程序。

60.什么是驱动程序?

驱动程序(Driver)是一种计算机程序或软件组件,它用于控制、管理或与硬件设备进行通信,以便操作系统或应用程序可以正确地与硬件设备进行交互。驱动程序充当操作系统和硬件之间的中间层,它使得应用程序能够与各种硬件设备(如打印机、显示器、声卡、网络适配器等)进行通信,而无需了解硬件的底层细节。

驱动程序的主要作用包括以下几个方面:

  1. 硬件控制: 驱动程序负责控制硬件设备的操作,包括启动、停止、配置、初始化和关闭等操作。它们将高级的命令转化为硬件可以理解的指令。

  2. 提供接口: 驱动程序提供了一个标准化的接口,使得操作系统和应用程序能够与硬件设备进行通信。这使得应用程序可以通过通用的API(应用程序编程接口)来访问硬件,而不必考虑不同硬件设备之间的差异。

  3. 硬件抽象: 驱动程序将硬件的细节抽象出来,隐藏了底层硬件的复杂性。这使得开发人员可以更容易地编写应用程序,而不必担心不同硬件平台之间的不同之处。

  4. 性能优化: 驱动程序通常会针对特定硬件设备进行性能优化,以确保硬件能够在最佳状态下运行。

  5. 错误处理: 驱动程序还负责处理硬件故障、错误和异常情况,以确保系统的稳定性和可靠性。

总之,驱动程序是操作系统和硬件之间的桥梁,它们允许软件与硬件设备进行交互,从而实现计算机系统的功能。不同的硬件设备通常需要不同的驱动程序来支持。

61.如何编写设备驱动程序?

了解硬件和设备规范: 在编写驱动程序之前,首先需要详细了解要支持的硬件设备的规格和工作原理。这包括设备的寄存器、通信协议、中断处理等信息。

创建驱动程序框架: 开始编写驱动程序时,建议首先创建一个基本的驱动程序框架,包括初始化、配置、设备打开/关闭、数据传输和错误处理等功能。

编写设备初始化代码: 驱动程序通常需要初始化硬件设备,包括配置寄存器、设置中断处理程序等。这是驱动程序的重要部分。

实现设备读写操作: 编写驱动程序的核心部分是实现设备的读写操作。这涉及到从设备接收数据和将数据发送到设备。

处理中断: 如果硬件设备支持中断,您需要编写中断处理程序来响应硬件中断事件。

调试和测试: 驱动程序开发通常伴随着大量的调试和测试工作。使用调试工具和模拟器来验证驱动程序的正确性和性能。

62.什么是内核态和用户态?

内核态(Kernel Mode)和用户态(User Mode)是操作系统中的两个不同的运行级别或权限级别,用于控制程序对计算机硬件资源的访问和操作。

  1. 内核态(Kernel Mode):

    • 内核态是操作系统的高权限运行级别。在内核态下,操作系统内核拥有对计算机的完全控制权,可以执行任何操作,包括直接访问硬件资源。
    • 内核态中运行的代码具有更高的特权级别,可以执行特权指令,如访问受保护的内存区域、处理中断和异常等。
    • 内核态通常用于执行操作系统内核代码和设备驱动程序,以及处理系统级任务。
  2. 用户态(User Mode):

    • 用户态是操作系统的低权限运行级别。在用户态下,程序的权限受到限制,不能直接访问硬件资源或执行特权指令。
    • 大多数应用程序在用户态下运行,它们通过系统调用(System Call)向操作系统请求服务,例如文件操作、网络通信等。
    • 用户态下的程序不能直接干扰或更改操作系统内核的状态,这是为了确保系统的稳定性和安全性。

在通常情况下,当程序在用户态运行时,它们不能执行直接的硬件操作,而必须通过操作系统来执行这些操作。当程序需要执行系统级任务时,它们通过系统调用进入内核态,内核执行请求的任务后再返回用户态。

切换运行级别(从用户态到内核态或相反)通常需要特殊的机制,如中断、异常和系统调用。这些机制确保了操作系统内核的稳定性和安全性,同时允许应用程序在受到限制的环境中运行。

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

答:mknod命令结合设备的主设备号和次设备号,可创建一个设备文件。

评:这只是其中一种方式,也叫手动创建设备文件。还有UDEV/MDEV自动创建设备文件的方式,UDEV/MDEV是运行在用户态的程序,可以动态管理设备文件,包括创建和删除设备文件,运行在用户态意味着系统要运行之后。那么在系统启动期间还有devfs创建了设备文件。一共有三种方式可以创建设备文件。

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

答:中断处理例程应该尽量短,把能放在后半段(tasklet,等待队列等)的任务尽量放在后半段。
评:写一个中断服务程序要注意快进快出,在中断服务程序里面尽量快速采集信息,包括硬件信息,然后推出中断,要做其它事情可以使用工作队列或者tasklet方式。也就是中断上半部和下半部。
第二:中断服务程序中不能有阻塞操作。为什么?大家可以讨论。

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

65.devfs创建设备文件

devfs(Device Filesystem)是一种用于在Unix-like操作系统中动态创建和管理设备文件的虚拟文件系统。它的目的是简化设备文件的管理和维护,特别是在Linux系统中。

与传统的设备文件管理方法相比,devfs具有以下特点:

  1. 动态设备文件创建: devfs允许在需要时自动创建设备文件,而不需要手动创建或管理这些文件。这使得添加、删除或更改设备驱动程序后的设备文件管理更加方便。

  2. 设备文件的虚拟化: devfs设备文件表示为虚拟目录和文件,这些虚拟文件可以在/dev目录中找到。设备文件以一种更具层次结构的方式组织,使得查找和访问设备文件更容易。

  3. 设备文件的自动销毁: 当设备不再被使用时,devfs可以自动销毁相关的设备文件,从而释放系统资源。

  4. 更好的设备文件命名约定: devfs使用一种更一致和直观的设备文件命名约定,使得用户和开发人员更容易理解设备的用途和特性。

需要注意的是,devfs在Linux中已被udev(设备管理守护程序)所取代,udev提供了更灵活和强大的设备文件管理机制。因此,在现代Linux系统中,你通常会遇到udev而不是devfs

总之,devfs和类似的设备文件系统简化了设备文件的管理,提高了系统的灵活性和可维护性。它们为设备驱动程序和用户空间应用程序提供了一种更方便的方式来访问硬件设备。

66. 中断服务程序中为什么不能有阻塞操作

中断服务程序(ISR,Interrupt Service Routine)是在响应中断时执行的程序,通常用于处理硬件事件、错误或其他异步事件。中断服务程序的设计要求必须尽量快速和可预测,因此不推荐在其中包含阻塞操作的原因有以下几点:

  1. 实时性需求: 中断通常用于处理需要及时响应的事件,如硬件故障、数据到达等。如果中断服务程序包含阻塞操作,将导致响应时间不可预测,无法满足实时性要求。

  2. 中断嵌套: 在某些系统中,中断是可嵌套的,即一个中断服务程序可以被另一个中断打断。如果一个中断服务程序被阻塞,而另一个中断到来,可能会导致复杂的竞争条件和问题。

  3. 资源争用: 阻塞操作可能涉及到对共享资源的访问,如内存、文件系统等。如果多个中断服务程序试图同时访问这些资源,可能引发竞争条件和死锁。

  4. 中断积压: 如果中断服务程序的执行时间过长,未处理的中断可能会积压起来,导致系统性能下降,并可能丢失某些重要事件。

因此,中断服务程序应该尽量保持简单、高效,只执行必要的操作,并且避免包含可能引起阻塞的代码。如果需要进行复杂的处理或长时间的操作,通常建议将这些工作移到一个低优先级的线程或任务中,以确保中断服务程序的快速响应和系统的可靠性。

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

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

在中断服务程序中,通常更推荐使用自旋锁而不是信号量,原因如下:

  1. 实时性要求: 中断服务程序通常需要快速响应,不能阻塞。自旋锁不会导致线程阻塞,因此更适合用于中断上下文。
  2. 简单性: 自旋锁的实现相对较简单,不涉及线程的上下文切换,适用于中断服务程序的要求。

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

原子操作指的是无法被打断的操作。当然不行,如果在这个变量的值被修改之前,线程发生了调度,另一个线程此时同样能进入临界区。

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

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

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

70.什么是 init 系统服务?

​ init(为英语:initialization 的简写)是 Unix 和 类 Unix 系统中用来产生其它所有进程的程序。它以守护进程的方式存在,其进程号为 1。Linux系统在引导时加载 Linux 内核后,便由 Linux 内核加载 init 程序,由 init 程序完成余下的引导过程,比如加载运行级别,加载服务,引导 Shell/图形化界面等等。

I2C

I2C 硬件框架

image-20230910201630927
在一个芯片(SoC)内部,有一个或多个 I2C 控制器
在一个 I2C 控制器上,可以连接一个或多个 I2C 设备
I2C 总线只需要 2 条线:时钟线 SCL、数据线 SDA
在 I2C 总线的 SCL、SDA 线上,都有上拉电阻

I2C 软件框架

image-20230910201743126

以 I2C 接口的存储设备 AT24C02 为例:

APP:需要调用设备驱动程序提供的接口

AT24C02 驱动:AT24C02 要求的地址、数据格式,发出什么信号才能让 AT24C02 执行擦除、烧写工作,判断数据是否烧写成功,构造好一系列的数据,发给 I2C 控制器

I2C 控制器驱动:它根据 I2C 协议发出各类信号:I2C 设备地址、I2C 存储地址、数据, 它根据 I2C 协议判断

IIC 传输数据的格式

1. 写操作

image-20230910202126332

2.读操作

image-20230910202153520

I2C 信号

I2C 协议中数据传输的单位是字节,也就是 8 位。但是要用到 9 个时钟:前面 8 个时钟用来传输 8 数据,第 9 个时钟用来传输回应信号。传输时,先传输最高位(MSB)。
⚫ 开始信号(S):SCL 为高电平时,SDA 高电平向低电平跳变,开始传送数据。
⚫ 结束信号(P):SCL 为高电平时,SDA 由低电平向高电平跳变,结束传送数据。
⚫ 响应信号(ACK):接收器在接收到 8 位数据后,在第 9 个时钟周期,拉低 SDA

SDA 上传输的数据必须在 SCL 为高电平期间保持稳定,SDA 上的数据只能在SCL 为低电平期间变化

I2C 协议信号如下:

image-20230910202827121

协议细节

⚫ 如何在 SDA 上实现双向传输?
◼ 主芯片通过一根 SDA 线既可以把数据发给从设备,也可以从 SDA 上读取数据,连接 SDA 线的引脚里面必然有两个引脚(发送引脚/接受引脚)。
⚫ 主、从设备都可以通过 SDA 发送数据,肯定不能同时发送数据,怎么错开时间?在 9 个时钟里:
◼ 前 8 个时钟由主设备发送数据的话,第 9 个时钟就由从设备发送数据;
◼ 前 8 个时钟由从设备发送数据的话,第 9 个时钟就由主设备发送数据。
⚫ 双方设备中,某个设备发送数据时,另一方怎样才能不影响 SDA 上的数据?
◼ 设备的 SDA 中有一个三极管,使用开极/开漏电路(三极管是开极,CMOS 管是开漏,作用一样),如下图:

image-20230910203130927

从真值表和电路图我们可以知道:
⚫ 当某一个芯片不想影响 SDA 线时,那就不驱动这个三极管
⚫ 想让 SDA 输出高电平,双方都不驱动三极管(SDA 通过上拉电阻变为高电平)
⚫ 想让 SDA 输出低电平,就驱动三极管

从 下 面 的 例 子 可 以 看 看 数 据 是 怎 么 传 的 ( 实 现 双 向 传 输 )。举例:主设备发送(8bit)给从设备

⚫ 前 8 个 clk
◼ 从设备不要影响 SDA,从设备不驱动三极管
◼ 主设备决定数据,主设备要发送 1 时不驱动三极管,要发送 0 时驱动
三极管
⚫ 第 9 个 clk,由从设备决定数据
◼ 主设备不驱动三极管
◼ 从设备决定数据,要发出回应信号的话,就驱动三极管让 SDA 变为 0从这里也可以知道 ACK 信号是低电平。

从上面的例子,就可以知道怎样在一条线上实现双向传输,这就是 SDA 上要使用上拉电阻的原因。
为何 SCL 也要使用上拉电阻?在第 9 个时钟之后,如果有某一方需要更多的 时 间 来 处 理 数 据 , 它 可 以 一 直 驱 动 三 极 管 把 SCL 拉 低 。当 SCL 为低电平时候,大家都不应该使用 IIC 总线,只有当 SCL 从低电平变为高电平的时候,IIC 总线才能被使用。
当它就绪后,就可以不再驱动三极管,这是上拉电阻把 SCL 变为高电平,其他设备就可以继续使用 I2C 总线了。
对于 IIC 协议它只能规定怎么传输数据,数据是什么含义由从设备决定。

SMBus 是 I2C 协议的一个子集

SMBus 为系统和电源管理这样的任务提供了一条控制总线,使用 SMBus 的系统,设备之间发送和接收消息都是通过 SMBus,而不是使用单独的控制线,这样可以节省设备的管脚数。SMBus 是基于 I2C 协议的,SMBus 要求更严格,SMBus 是 I2C 协议的子集。

使用一句话概括 I2C 传输:APP 通过 I2C Controller 与 I2C Device 传输数据。

源码流程分析

1 使用 I2C 方式

示例代码:i2ctransfer.c

image-20230910215432531

2 使用 SMBus 方式

示例代码:i2cget.c、i2cset.c

image-20230910215523464

71. 怎么编写驱动程序

① 确定主设备号,也可以让内核分配
② 定义自己的 file_operations 结构体
③ 实现对应的 drv_open/drv_read/drv_write 等函数,填入 file_operations 结构体
④ 把 file_operations 结构体告诉内核:register_chrdev
⑤ 谁来注册驱动程序啊?得有一个入口函数:安装驱动程序时,就会去调用这个入口函数
⑥ 有入口函数就应该有出口函数:卸载驱动程序时,出口函数调用unregister_chrdev
⑦ 其他完善:提供设备信息,自动创建设备节点:class_create,device_create

72. 编译驱动

先设置好交叉编译工具链,配置、编译好你的板子所用的内核,然后修改Makefile 指定内核源码路径,最后即可执行 make 命令编译驱动程序和测试程序。

我们是在 Ubuntu 中编译程序,但是需要在 ARM 板子上测试。所以需要把程序放到 ARM 板子上。
启动单板后,可以通过 NFS 挂载 Ubuntu 的某个目录,访问该目录中的程序。

# echo "7 4 1 7" > /proc/sys/kernel/printk // 打开内核的打印信息,有些板子默认打开了

73.驱动编写的三种方法

1. 传统写法

image-20230910230926982

2. 总线设备驱动模型

image-20230910231001130

引入 platform_device/platform_driver,将“资源”与“驱动”分离开来。

3. 设备树

image-20230910231241688

通过配置文件──设备树来定义“资源”。
代码稍微复杂,但是易于扩展。
无冗余代码,修改引脚时只需要修改 dts 文件并编译得到 dtb 文件,把它传给内核。
无需重新编译内核/驱动。

74.设备树的引入与作用

设备树只是用来给内核里的驱动程序,指定硬件的信息。比如 LED 驱动,在内核的驱动程序里去操作寄存器,但是操作哪一个引脚?这由设备树指定。

/sys/firmware/devicetree 目录下是以目录结构程现的 dtb 文件, 根节点对应 base 目录, 每一个节点对应一个目录, 每一个属性对应一个文件。
这些属性的值如果是字符串,可以使用 cat 命令把它打印出来;对于数值,可以用 hexdump 把它打印出来。
一个单板启动时,u-boot 先运行,它的作用是启动内核。U-boot 会把内核和设备树文件都读入内存,然后启动内核。在启动内核时会把设备树在内存中的地址告诉内核。

设备树的语法

为什么叫“树”?

image-20230910232333962

我们需要编写设备树文件(dts: device tree source),它需要编译为dtb(device tree blob)文件,内核使用的是 dtb 文件。

内核对设备树的处理

image-20230910232843313

75. platform_device 如何与 platform_driver 配对

从设备树转换得来的 platform_device 会被注册进内核里,以后当我们每注册一个 platform_driver 时,它们就会两两确定能否配对,如果能配对成功就调用 platform_driver 的 probe 函数。套路是一样的。我们需要将前面讲过的“匹配规则”再完善一下,先贴源码:

image-20230911091429447

1 最先比较:是否强制选择某个 driver

比较:

platform_device.driver_override 和 platform_driver.driver.name

可以设置 platform_device 的 driver_override强制选择某个 platform_driver

2 然后比较:设备树信息

platform_device.dev.of_node 和 platform_driver.driver.of_match_table 。
由设备树节点转换得来的 platform_device 中,含有一个结构体:of_node。
它的类型如下:

platform_device.dev.of_node 和 platform_driver.driver.of_match_table 。
由设备树节点转换得来的 platform_device 中,含有一个结构体:of_node。
它的类型如下:

image-20230911092239877

image-20230911092702635

使用设备树信息来判断 dev 和 drv 是否配对时:
① 首先,如果 of_match_table 中含有 compatible 值,就跟 dev 的 compatile属性比较,若一致则成功,否则返回失败;
② 其次,如果 of_match_table 中含有 type 值,就跟 dev 的 device_type 属性比较,若一致则成功,否则返回失败;
③ 最后,如果 of_match_table 中含有 name 值,就跟 dev 的 name 属性比较,若一致则成功,否则返回失败。
而设备树中建议不再使用 devcie_type 和 name 属性,所以基本上只使用设备节点的 compatible 属性来寻找匹配的 platform_driver。

3 接下来比较:platform_device_id

比较 platform_device.name 和platform_driver.id_table[i].name,id_table 中可能有多项。

4. 4 最后比较

platform_device.name 和 platform_driver.driver.name

76. 总结 3 种写驱动程序的方法

1 资源和驱动在同一个文件里

image-20230911095025413

2.资源用 platform_device 指定、驱动在 platform_driver 实现

image-20230911095042921

3.资源用设备树指定,驱动在 platform_driver 实现

image-20230911095102258

77 .怎么使用设备树写驱动程序

1 设备树节点要与 platform_driver 匹配

  • 设备树要有 compatible 属性,它的值是一个字符串
  • platform_driver 中要有 of_match_table,其中一项的.compatible 成员设置为一个字符串
  • 上述 2 个字符串要一致。

2 设备树节点指定资源,platform_driver 获得资源

​ 如 果 在 设 备 树 节 点 里 使 用 reg 属 性 , 那 么 内 核 生 成 对 应 的platform_device 时会用 reg 属性来设置 IORESOURCE_MEM 类型的资源。如 果 在 设 备 树 节 点 里 使 用 interrupts 属 性 , 那 么 内 核 生 成 对 应 的platform_device 时会用 reg 属性来设置 IORESOURCE_IRQ 类型的资源。对于interrupts 属性,内核会检查它的有效性,所以不建议在设备树里使用该属性来表示其他资源。

78. 函数指针

把函数的指针(地址)作为参数传递给另一个函数,当这个指针被用来调用其所指向的函数时,我们就说这是回调函数。

79. Linux内核体系结构

Linux内核主要由5个模块构成,进程调度模块、内存管理模块、文件系统模块、进程间通信模块、网络接口模块。

80. Linux的内核内存分布是怎么样的?

Linux内核内存分布通常可以分为以下几个主要区域:

  1. 内核空间(Kernel Space):这部分内存是专门用于存放操作系统内核的代码和数据结构的区域。它通常占用高地址的内存范围,从虚拟内存地址的最高端开始,向下分配一定的地址空间。在这个区域中,包含了操作系统内核的代码、数据结构、驱动程序等。这部分内存是受保护的,用户程序不能直接访问。

  2. 用户空间(User Space):用户程序运行的内存空间,通常占用低地址的内存范围。在用户空间中,存放着用户应用程序的代码和数据。用户程序可以访问用户空间的内存,但不能直接访问内核空间的内存。

  3. 共享内存(Shared Memory):共享内存是一种特殊的内存区域,允许多个进程共享同一块物理内存区域。这在进程间通信中非常有用,可以提高通信效率。

  4. 内存映射(Memory Mapped):Linux支持内存映射技术,允许将文件映射到内存中,使得文件的读写操作可以像内存操作一样高效。这个区域通常位于用户空间,但可以通过内存映射访问文件。

  5. 栈区域(Stack):栈是用于存放函数调用信息、局部变量等的内存区域。每个线程都有自己的栈,用于管理函数调用和局部变量的生命周期。

  6. 堆区域(Heap):堆是用于动态内存分配的内存区域。在堆中,程序可以动态分配和释放内存,通常用于存放动态分配的数据结构,如链表、树等。

  7. 内存缓冲区(Memory Buffers):用于缓存文件数据或网络数据的内存区域。这些缓冲区通常由操作系统或应用程序使用,以提高数据读写性能。

总体而言,Linux的内核内存分布设计得相对较为灵活,允许不同的内存区域用于不同的目的,以满足不同应用场景的需求。不同的内存区域在内存分配、保护和访问控制方面有不同的特性和限制。

81. 段页式内存管理

在段页式系统中,作业的逻辑地址分为三部分:段号、页号和页内偏移量,如图所示。

在这里插入图片描述

为了实现地址变换,系统为每个进程建立一张段表,而每个分段有一张页表(在一个进程中,段表只有一个,而页表可能有多个)。段表表项中至少包括段号、页表长度和页表起始地址,页表表项中至少包括页号和块号。此外,系统中还应有一个段表寄存器,指出作业的段表起始地址和段表长度。

在进行地址变换时,首先通过段表查到页表起始地址,然后通过页表找到页帧号,最后形成物理地址。如图所示,进行一次访问实际需要三次访问主存,这里同样可以使用快表以加快查找速度,其关键字由段号、页号组成,值是对应的页帧号和保护码。

在这里插入图片描述

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 lk
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信