设为首页 加入收藏

TOP

调试linux内核(2): poll系统调用的实现(二)
2023-08-26 21:10:19 】 浏览:82
Tags:调试 linux 内核 poll
驱动代码可以在write接口中对这个等待队列进行唤醒操作, 从而实现唤醒进程. 具体的数据结构细节在后面的调试过程中展开.

问题3

怎样调试从进程发起poll调用到返回的过程?

这里构造的场景如下:

  1. 实现一个字符设备驱动, 驱动中实现了poll, write, read接口;
  2. 在内核中插入该模块, 在/dev下生成设备文件节点;
  3. 启动一个用户态进程, 并让它后台运行, 在进程中打开设备文件, 对该文件进行poll操作, 开始时设备数据为空, 进程将睡眠
  4. 使用echo命令向设备文件写入数据, 驱动的write接口被调用, 睡眠的进程被唤醒, 并读取设备数据;

在对poll的实现有了一个基本了解之后, 调试面临的第一个问题就是找到这个系统调用的入口, 这里提供两个调试技巧:

  1. 你知道在linux内核中系统调用使用SYSCALL_DEFINEx宏定义, 可以直接在代码中用正则表达式SYSCALL_DEFINE.*poll去搜索poll系统调用的位置, 然后在入口打断点, 开始调试即可.
  2. 你不知道poll的入口在那, 但是在你的字符设备驱动中实现了poll接口, 这个接口一定会出现在poll系统调用的调用链上, 可以在你的驱动模块上打断点, 断点命中之后, 看调用栈找到syscall的入口, 再进一步调试. 这种方法需要借助内核提供的gdb脚本加载驱动模块的调试信息, 否则gdb无法获得指令和源文件中各行的对应信息以及其他的符号信息.

关于调试的环境问题, 可以参考之前的文章, 以下是调试过程的视频记录:

<iframe src="//player.bilibili.com/player.html?aid=405173438&bvid=BV1NG411Z7qu&cid=1245867597&page=1" scrolling="no" border="0" frameborder="no" framespacing="0" allowfullscreen="true" width="100%" height="600">

poll系统调用涉及到的重要数据结构, 以及它们之间的关系总结如下:
image

在逻辑上可以分成如图所示的两个部分, 分别和poll系统调用的上层实现以及驱动模块的poll接口实现相关. 各数据结构的作用如下:

  1. 进程进入poll系统调用时, 内核对poll_wqueues的各个成员进行初始化, 包括:

    • 用一个默认的函数初始化pt的_qproc函数指针;
    • 用current初始化polling_task, 记录发起poll系统调用的进程;
    • 虚线框中的成员嵌套着wait_queue_entry, 这个被嵌套的数据类型, 是将来真正插入到驱动模块提供的等待队列wait_queue_head的节点;
  2. 当驱动模块的poll接口被上层调用时:

    • 驱动代码需要调用poll_wait函数, 以自己维护的等待队列wait_queue_head作为参数, 并透传poll_table指针和file指针;
      • 在poll_wait的实现中, 会检查poll_table的_qproc是否为空, 不为空则继续透传参数, 调用_qproc;
        • 在_qproc中, 会从poll_wqueues中获取一个空闲的poll_table_entry, 初始化图中的三个成员, 其中的wait_queue_entry:
          • private指针被设为poll_wqueues的地址, 这样将来被唤醒时就可以找到之前睡眠的进程, 也就是polling_task;
          • func被设为一个默认的函数,将来这个节点所属的等待队列被唤醒时, func被调用, 根据private指针找到要唤醒的进程;
          • 通过链表操作, 将节点插入到等待队列中;
  3. 当有数据写入设备时:
    驱动模块检测到设备有数据可读了, 需要唤醒传递给poll_wait的等待队列, 这时队列上每个节点的func都会被调用, 最终之前睡眠的进程被唤醒;

  4. 当设备可写时, 唤醒过程类似, 只是使用的队列不同.

概括下来:

  • 驱动模块只要维护自己的等待队列, 在poll接口的实现中, 调用上层提供的poll_wait向队列中插入元素, 并返回当前的设备状态;
  • 驱动的其他部分在合适的时机对等待队列执行唤醒操作;
  • poll系统调用的上层实现代码, 负责维护一套数据结构, 记录插入到等待队列中的节点, 给节点进行必要的设置, 使得通过节点能够唤醒正确的进程;

总结

设备驱动的开发是在内核提供的框架下进行的, 为了降低驱动的开发难度, 快速支持各种新设备, 这套框架的设计必然要经得住考验, 这也导致驱动的开发存在很多模板一样的套路, 有人戏称为"完形填空". 但是以驱动开发为出发点, 深入了解内核的各个模块, 个人感觉是学习linux的一个很好的方式. 欢迎加入技术讨论qq群: 838923389 一起研究linux相关的底层技术.

首页 上一页 1 2 下一页 尾页 2/2/2
】【打印繁体】【投稿】【收藏】 【推荐】【举报】【评论】 【关闭】 【返回顶部
上一篇系统内存管理:虚拟内存、内存分.. 下一篇SELinux 入门 pt.2

最新文章

热门文章

Hot 文章

Python

C 语言

C++基础

大数据基础

linux编程基础

C/C++面试题目