Linux学习笔记——内核态与用户态

发布于:2025-04-11 ⋅ 阅读:(228) ⋅ 点赞:(0)

内存的划分不仅涉及到地址空间,还与进程的执行模式密切相关,这就是内核态和用户态的概念。

1. 内核态(Kernel Mode)

当进程运行在内核空间时,它处于内核态(Ring0)。此时,进程具有完全的控制权,能够执行任何指令,包括特权指令(如访问硬件、修改内存等),并且可以访问整个系统的资源。

  • 特点:
    • 可以直接访问硬件资源。
    • 运行时没有任何限制,能够执行任何指令。
    • 系统中只有操作系统核心代码能以此方式运行,避免普通用户程序直接访问硬件。

2. 用户态(User Mode)

当进程运行在用户空间时,它处于用户态(Ring3)。此时,进程只能执行非特权指令,并且只能访问映射到自己用户空间中的内存区域,不能直接访问硬件资源。

  • 特点:
    • 只能访问自己的用户空间,无法直接访问操作系统内核。
    • 通过系统调用与内核进行交互。
    • 如果发生错误,用户态的进程不会直接影响操作系统的稳定性。

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

  1. 安全性:通过将操作系统内核和用户应用程序的内存空间分开,可以避免普通程序错误或恶意程序影响到操作系统的稳定性和安全性。只有内核态才能访问系统核心资源,普通用户程序不能轻易损害系统。

  2. 稳定性:在内核态,操作系统可以运行在更高的权限级别,保证系统的稳定性。即使某个用户程序崩溃,也不会导致整个操作系统崩溃。不同的用户程序也可以独立运行而不干扰彼此。

  3. 资源管理:操作系统通过内核空间来管理硬件资源和系统资源,用户程序不能直接操作这些资源,避免了资源冲突和不当使用。

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

由于内核空间的特殊性,普通用户进程无法直接访问内核空间,因此需要通过特定的机制来进行交互。下面列出了几种常见的用户空间与内核空间通信方式:

1. 使用 API

get_user 和 put_user 是内核中提供的 API 函数,用于在用户空间和内核空间之间传递数据。

  • get_user(x, ptr):在内核中调用,将用户空间地址 ptr 处的数据读取到内核变量 x 中。
  • put_user(x, ptr):在内核中调用,将内核变量 x 的数据写入到用户空间地址 ptr。

此外,copy_from_user() 和 copy_to_user() 也常用于内核与用户空间之间的数据传输,尤其在设备驱动的读写操作中,通过系统调用触发这些操作。

  • copy_from_user():从用户空间复制数据到内核空间。
  • copy_to_user():从内核空间复制数据到用户空间。

这些 API 函数通常用于内核驱动开发中,提供了一种简单的方式来访问用户空间中的数据。

2. 使用 /proc 文件系统

/proc 文件系统是一个虚拟文件系统,用于内核与用户空间之间的通信。/proc 文件系统中的文件不是普通文件,而是由内核动态生成的虚拟文件,用户可以通过读取或写入这些文件来与内核交换信息。

  • 例如,通过创建一个新的 /proc 条目,用户可以在应用程序中访问内核中动态生成的内容。内核可以创建一个文件条目,然后用户进程通过读取该文件来获取内核空间的数据,或通过写入该文件来向内核传递数据。

在 Linux 中,通常使用 create_proc_entry() 函数来创建这些条目,并通过返回的指针操作相关数据。

3. 使用 sysfs 文件系统与 kobject

sysfs 文件系统是另一种虚拟文件系统,主要用于在内核与用户空间之间提供更结构化的交互。每个在内核中注册的 kobject 都会在 /sys 目录下创建一个对应的条目,用户进程可以通过读取或写入这些条目与内核进行通信。

  • kobject 是内核中的一种对象结构体,它表示一个内核中的对象,可以通过 kobject 在 sysfs 中创建、删除和访问虚拟文件系统中的目录和文件。

这种机制被广泛用于硬件设备管理,例如,在 /sys/class 目录下,用户可以访问设备驱动程序的属性并进行修改。

4. 使用 netlink

Netlink 是 Linux 提供的一种机制,允许用户空间和内核空间通过 socket 进行通信。Netlink 提供了类似于 BSD socket 的 API,用于内核和用户之间的进程间通信(IPC)。

  • Netlink 的优势包括:
    • 支持自定义协议,避免了添加额外的文件系统。
    • 支持多点传送,即一个消息可以同时发送给多个接收方。
    • 支持内核先发起会话,允许内核主动与用户进程通信。
    • 异步通信模式,支持消息缓存机制。

它常用于网络子系统和系统事件的通知,例如,内核发送网络事件到用户空间应用程序。

5. 使用文件系统

这种方式相对较为原始,但它确实可以通过操作文件来实现内核与用户空间的通信。内核空间中的程序会将数据写入一个文件,然后用户空间的程序读取该文件来获取信息。

  • 例如,内核可以向 /home/user/str_from_kernel 文件中写入数据,然后用户程序读取该文件来获取数据。

虽然这种方式比较笨拙,但在某些简单的情况下,它仍然是一种有效的通信方式。

6. 使用 mmap 系统调用

mmap 系统调用可以将内核空间的物理内存映射到用户空间。通过这种方式,用户空间可以直接访问内核空间的内存。

  • 在某些设备驱动程序中,mmap 可以用于将内核空间的特定区域(如设备内存或共享内存)映射到用户进程的虚拟地址空间,从而实现高效的内核与用户空间数据交换。
  • 除了映射内存区域外,也可以通过重写 file_operations 中的 mmap 函数来实现特定的映射行为。

这种方式通常用于高性能应用程序,尤其是在涉及设备或共享内存时。

7. 使用信号

信号是操作系统中用于通知进程某些事件的机制。内核可以向用户空间进程发送信号,通知其发生了某些特定事件或错误。

  • 比如,当用户程序发生错误时,内核可以发送信号(如 SIGSEGV)来终止进程的执行。
  • 信号也可以用于内核与用户程序之间的简单异步通信。

总结

内核空间与用户空间的通信机制多种多样,每种机制都有不同的使用场景和特点:

  • API(如 get_user, put_user)用于在内核与用户之间进行直接的数据传递。
  • /proc 文件系统 和 sysfs 文件系统 提供了通过虚拟文件系统的方式进行交互。
  • Netlink 提供了一种基于 socket 的高效通信方式,适用于网络通信和事件通知。
  • 文件系统 作为一种简陋但有效的通信方式,适用于简单的数据交换。
  • mmap 提供了内核与用户空间之间的直接内存映射,适用于高效数据交换。
  • 信号 用于内核向用户进程发送事件通知。

内核链表的通用性

内核链表具有通用性,主要是由于其设计为双向循环链表,并且每个节点仅包含指针域,没有任何数据域。这种设计赋予了内核链表高度的灵活性和适应性,使得它能够广泛应用于内核中管理各种类型的设备和资源。以下是内核链表通用性的几个关键特点:

  1. 没有数据域的节点设计
    内核链表的每个节点只有前后指针,没有数据域。这意味着链表的结构与具体的数据内容无关。因此,任何数据结构都可以轻松地被纳入链表管理,只要在数据结构中包含适当的链表节点字段。这样,无论数据结构多复杂,都可以通过链表进行统一管理。例如,在设备管理中,设备描述结构体(如 struct device)只需包含一个链表节点(如 list_head)字段,即可方便地将其加入到内核链表中。

  2. 结构体通过链表字段与链表关联
    任何需要被链表管理的数据结构,只需在其内部包含一个指向链表的指针(如 struct list_head)。这个指针用于将数据结构链接到链表的相应位置,从而使内核可以方便地遍历和操作这些数据结构。

  3. 双向循环链表的遍历便利性
    由于内核链表是双向链表,每个节点有两个指针(指向前一个节点和后一个节点),这使得从任意节点出发,都可以双向遍历整个链表。此外,循环链表的设计使得遍历过程更加方便,可以在遍历到链表尾部时继续从头部开始循环。这样的设计非常适合需要不断遍历的场景,例如进程调度等。

  4. 循环遍历的优点
    循环链表的特点使得它特别适用于那些需要循环操作的数据结构,如进程调度。操作系统将就绪的进程放入一个循环链表中,通过不断地轮询链表,操作系统可以不断地为每个进程分配时间片,从而实现进程的循环调度。

应用程序执行 open() 时从用户空间到内核空间的流程

在 Linux 中,当应用程序执行 open() 系统调用时,涉及从用户空间到内核空间的多个步骤。下面详细描述了这一过程:

  1. 应用层调用 open() 函数
    当应用程序调用 open() 时,它首先会通过 VFS(虚拟文件系统)层进行处理。VFS 通过文件路径名找到对应的设备或文件,并查找其相关的 struct inode 结构体。struct inode 包含了文件的元数据(如文件权限、大小等),并且它还指向具体的设备类型(字符设备或块设备)。

  2. 从 VFS 到驱动层的过渡
    一旦 VFS 找到 struct inode,它会进一步根据设备号(dev_t)来查找对应的设备驱动程序。对于字符设备,每个设备都会有一个对应的 struct cdev 结构体,struct cdev 描述了该设备的所有信息,包括该设备的操作函数接口(如 open、read、write 等函数)。

  3. 通过 struct cdev 连接 VFS 和驱动层
    在 VFS 层,struct inode 中有一个指向 struct cdev 结构体的字段(i_cdev),它将 VFS 和设备驱动层连接起来。struct cdev 中记录了设备的操作接口函数,VFS 层通过这些接口函数调用设备驱动程序来执行具体的操作。

  4. 通过 struct file 进行操作接口链接
    struct file 是内核中用于表示文件的一个数据结构,它在 VFS 层和驱动层之间起到了桥梁作用。在 VFS 层,struct file 结构体包含了操作文件的函数指针,例如 open、read、write 等。通过 struct file 指针,VFS 可以找到并调用对应设备的操作函数,从而执行实际的设备操作。

  5. 返回文件描述符
    当 open() 操作完成后,VFS 层会返回一个文件描述符(fd)给应用程序。这个文件描述符实际上是一个指向 struct file 结构体的指针,应用程序可以通过这个文件描述符来进一步操作文件或设备。

总结来说,内核链表由于其通用的设计,可以轻松地管理各种不同类型的资源,并在内核中进行高效的遍历和调度。而应用程序从用户空间调用 open() 时,内核通过一系列结构体(如 inode、cdev 和 file)的链接,完成设备的打开操作。


微信公众号

今日签到

点亮在社区的每一天
去签到