字符设备驱动之从用户程序中的系统调用到驱动中的具体实现

  引:我们知道每一个字符设备在内核中都有一个cdev结构来描述之,而这个结构比较重要的一个成员就是

const struct file_operations *ops;

该结构的作用是将用户程序中的系统调用和驱动程序中的具体实现函数一一对应起来。当在用户程序中对一个字符设备文件调用某一系统调用时,就知道该对这个字符设备调用哪个具体的函数,但是问题来了,下面看两个函数原型:

//这是read系统调用的原型
ssize_t read(int fd, void *buf, size_t count);
//这是file_operations中对应read的函数原型
ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);

可以看出,这两个函数的接口是不一样的,所以从系统调用到驱动中的具体实现,内核肯定是做了一些手脚的,下面我们就通过代码来分析从系统调用到驱动中具体实现函数的历程。

//这是一个简单的从文件中读取数据的程序,我们主要通过它来分析read系统调用的历程
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>

int main(int argc, char const *argv[])
{
    int fd;
    int temp;
    if((fd = open("/dev/memdev0", O_RDWR)) == -1)
        printf("open failed at line %d\n", __LINE__);
    read(fd, &temp, sizeof(int));
        printf("read failed at line %d\n", __LINE__);

    printf("buffer is %d\n", temp);

    close(fd);

    return 0;
}

编译上面的代码,注意一定要采用静态编译,否则在之后的反汇编中会失去很重要的信息。

然后反汇编,导入一个dump文件中下面是read函数对应的汇编代码

read(fd, &temp, sizeof(int));
    8268:    e24b300c     sub    r3, fp, #12    ; 0xc
    826c:    e51b0008     ldr    r0, [fp, #-8]
    8270:    e1a01003     mov    r1, r3
    8274:    e3a02004     mov    r2, #4    ; 0x4
    8278:    eb0028e8     bl    12620 <__libc_read>

开始是传参数,最后一句跳转到了__libc_read标号处,继续跟踪

00012620 <__libc_read>:
   12620:    e51fc028     ldr    ip, [pc, #-40]    ; 12600 <__libc_close+0x70>
   12624:    e79fc00c     ldr    ip, [pc, ip]
   12628:    e33c0000     teq    ip, #0    ; 0x0
   1262c:    1a000006     bne    1264c <__libc_read+0x2c>
   12630:    e1a0c007     mov    ip, r7
   12634:    e3a07003     mov    r7, #3    ; 0x3
   12638:    ef000000     svc    0x00000000
   1263c:    e1a0700c     mov    r7, ip
   12640:    e3700a01     cmn    r0, #4096    ; 0x1000
   12644:    312fff1e     bxcc    lr
   12648:    ea0008b4     b    14920 <__syscall_error>
   1264c:    e92d408f     push    {r0, r1, r2, r3, r7, lr}
   12650:    eb0003b9     bl    1353c <__libc_enable_asynccancel>
   12654:    e1a0c000     mov    ip, r0
   12658:    e8bd000f     pop    {r0, r1, r2, r3}
   1265c:    e3a07003     mov    r7, #3    ; 0x3
   12660:    ef000000     svc    0x00000000
   12664:    e1a07000     mov    r7, r0
   12668:    e1a0000c     mov    r0, ip
   1266c:    eb000396     bl    134cc <__libc_disable_asynccancel>
   12670:    e1a00007     mov    r0, r7
   12674:    e8bd4080     pop    {r7, lr}
   12678:    e3700a01     cmn    r0, #4096    ; 0x1000
   1267c:    312fff1e     bxcc    lr
   12680:    ea0008a6     b    14920 <__syscall_error>
   12684:    e1a00000     nop            (mov r0,r0)
   12688:    e1a00000     nop            (mov r0,r0)
   1268c:    e1a00000     nop            (mov r0,r0)

代码很多,不过重要的只有两行,已经红色高亮显示出来了,具体做的事就是将3存入了r7寄存器中,然后通过svc指令将PC指针由用户空间从一个固定的入口跳入内核空间,之后内核根据r7中的编号,查表后执行对应的系统调用。在linux2.6.39版本的内核中,该表位于calls.S这个文件中

//这是该文件的部分代码
/* 0 */        CALL(sys_restart_syscall)
        CALL(sys_exit)
        CALL(sys_fork_wrapper)
        CALL(sys_read)
        CALL(sys_write)

可知用户空间中的read其实是调用了内核空间中的sys_read函数,该函数的实现代码如下

SYSCALL_DEFINE3(read, unsigned int, fd, char __user *, buf, size_t, count)
{
    struct file *file;
    ssize_t ret = -EBADF;
    int fput_needed;

    file = fget_light(fd, &fput_needed);
    if (file) {
        loff_t pos = file_pos_read(file);
        ret = vfs_read(file, buf, count, &pos);
        file_pos_write(file, pos);
        fput_light(file, fput_needed);
    }

    return ret;
}

分析:我们知道没一个打开的文件在内核中都有一个file结构体来维护,这里就调用fget_light通过read函数提供的fd得到了该文件的file结构体,然后调用了vfs_read函数

ssize_t vfs_read(struct file *file, char __user *buf, size_t count, loff_t *pos)
{
    ssize_t ret;

    if (!(file->f_mode & FMODE_READ))
        return -EBADF;
    if (!file->f_op || (!file->f_op->read && !file->f_op->aio_read))
        return -EINVAL;
    if (unlikely(!access_ok(VERIFY_WRITE, buf, count)))
        return -EFAULT;

    ret = rw_verify_area(READ, file, pos, count);
    if (ret >= 0) {
        count = ret;
        if (file->f_op->read)
            ret = file->f_op->read(file, buf, count, pos);
        else
            ret = do_sync_read(file, buf, count, pos);
        if (ret > 0) {
            fsnotify_access(file);
            add_rchar(current, ret);
        }
        inc_syscr(current);
    }

    return ret;
}

同样,最重要的代码已经高亮显示出来了,这就是整个历程最重要的一句,它调用了file里的成员函数f_op->read。在驱动初始化的时候就将file_operations结构体赋值给了这个驱动对应的设备文件的成员f_op,所以这里就掉用到了最终的驱动程序中的具体实现函数:xxx_read。其实红色的话我目前是找不到内核代码来证明的,不过逻辑就是这样,而且作为新手在学习驱动期间不要过分的去看内核代码,得不偿失。不过哪位大神如果能够找到相关的代码,感激不尽!

  如有疑问或错误,欢迎讨论!转载请注明出处!

时间: 2024-10-22 00:58:45

字符设备驱动之从用户程序中的系统调用到驱动中的具体实现的相关文章

Linux字符设备中的两个重要结构体(file、inode)

对于Linux系统中,一般字符设备和驱动之间的函数调用关系如下图所示 上图描述了用户空间应用程序通过系统调用来调用程序的过程.一般而言在驱动程序的设计中,会关系 struct file 和 struct inode 这两个结构体. 用户空间使用open()系统调用函数打开一个字符设备时( int fd = open("dev/demo", O_RDWR) )大致有以下过程: 在虚拟文件系统VFS中的查找对应与字符设备对应 struct inode节点 遍历字符设备列表(chardevs

字符设备驱动之Led驱动学习记录

一.概述 Linux内核就是由各种驱动组成的,内核源码中大约有85%的各种渠道程序的代码.一般来说,编写Linux设备驱动大致流程如下: 1.查看原理图,数据手册,了解设备的操作方法. 2.在内核中找到相近的驱动程序,以它为模板开发. 3.实现驱动的初始化:比如像内核注册这个驱动程序 4.设计要实现的操作:open,close,read,write等 5.实现中断服务(不是必须的) 6.编译该驱动程序到内核中,或insmod命令加载 7.测试驱动程序. 二.驱动程序的加载与卸载 module_i

Linux字符设备简单示例

1. Linux字符设备是一种按字节来访问的设备,字符驱动则负责驱动字符设备,这样的驱动通常实现open.close.read和write系统调用.例如:串口.Led.按键等. 2. 通过字符设备文件(/dev/),应用程序可以使用相应的字符设备驱动来控制字符设备 3. 创建字符设备文件的方法一般有两种 (1)使用命令mknod : mknod /dev/文件名  c 主设备号 次设备号 (查看主设备号:cat /proc/devices) (2)使用函数在驱动程序中创建 4. 字符设备通用设计

Linux字符设备驱动框架

字符设备是Linux三大设备之一(另外两种是块设备,网络设备),字符设备就是字节流形式通讯的I/O设备,绝大部分设备都是字符设备,常见的字符设备包括鼠标.键盘.显示器.串口等等,当我们执行ls -l /dev的时候,就能看到大量的设备文件,c就是字符设备,b就是块设备,网络设备没有对应的设备文件.编写一个外部模块的字符设备驱动,除了要实现编写一个模块所需要的代码之外,还需要编写作为一个字符设备的代码. 驱动模型 Linux一切皆文件,那么作为一个设备文件,它的操作方法接口封装在struct fi

深入浅出~Linux设备驱动之字符设备驱动

一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流的设备,常见的字符设备有鼠标.键盘.串口.控制台和LED设备等. 块设备:是指可以从设备的任意位置读取一定长度数据的设备.块设备包括硬盘.磁盘.U盘和SD卡等. 每一个字符设备或块设备都在/dev目录下对应一个设备文件.linux用户程序通过设备文件(或称设备节点)来使用驱动程序操作字符设备和块设备

LDD3阅读笔记-字符设备驱动

主要开发流程介绍 module_init宏和module_exit宏 当模块装载时需要调用module_init宏指定的函数,卸载时需要调用 module_exit宏指定的函数 以下是简单的init流程: 初始化设备 初始化file_operation 获取字符设备号 注册字符设备 当卸载模块时,需要释放申请的设备号. 主设备号和次设备号 对字符设备的访问是通过文件系统内的设备名称进行的.那些名称被称为特殊 文件.设备文件,或者简单称为文件系统树的节点,他们通常位于/dev目录. 通常而言,主设

linux 驱动学习(一)简单的字符设备驱动程序

linux 系统将设备分为三种类型:字符设备.块设备和网络接口设备. 文章将先给出字符设备驱动程序,参照程序记录知识点,可能会不全,以后会慢慢加 .知识点记录完成后,会贴出字符设备驱动程序的测试程序并记录测试过程. 注释版 1 #include "linux/kernel.h" //内核头文件,含有一些内核常用函数的原形定义 2 #include "linux/module.h" //包含大量加载模块需要的函数和符号的定义 3 #include "linu

【驱动】——字符设备驱动程序

字符设备不得不说的那些事: 一: 设备号:主设备号,次设备号: 数据类型 dev_t(unsigned int) 定义设备号  高12位主设备号 低20位次设备号: 二: 设备号的作用: 应用程序通过主设备号找到驱动程序: 三:如何分配设备号: ①:静态分配: 1: cat /proc/devices 查看linux系统哪个设备号没有被占用: 2: dev_t dev_id = MKDEV(主设备号,次设备号)  根据你的设备个数分配次设备号 如果设备个数只有一个,一般此设备号从0开始: 3: 

【转】深入浅出:Linux设备驱动之字符设备驱动

深入浅出:Linux设备驱动之字符设备驱动 一.linux系统将设备分为3类:字符设备.块设备.网络设备.使用驱动程序: 字符设备:是指只能一个字节一个字节读写的设备,不能随机读取设备内存中的某一数据,读取数据需要按照先后数据.字符设备是面向流的设备,常见的字符设备有鼠标.键盘.串口.控制台和LED设备等. 块设备:是指可以从设备的任意位置读取一定长度数据的设备.块设备包括硬盘.磁盘.U盘和SD卡等. 每一个字符设备或块设备都在/dev目录下对应一个设备文件.linux用户程序通过设备文件(或称