Hello驱动

从零开始编写一个Hello驱动,超超超超详细版(内核相关知识,环境配置,代码分析)

​ 最终的目标:编写一个hello驱动,将其加载进内核,创建hello_dev,把一个字符串正序写入hello_dev,再逆序读出来,输出读取结果。

一 前菜:驱动相关知识

​ 在linux下,一切都是文件,这句话从一开始接触linux就知道了,也自己以为理解了,但到现在我才发现自己压根没整明白。

设备驱动是一段代码,这段代码不会自己去执行,只会等待内核调用,内核在打开某个设备文件时,会调用该设备相应驱动的代码

​ 接下来的事情就是把这句话彻底整明白。

1.1 驱动程序

​ 驱动程序,就是能够对某个设备进行的操作的集合,linux下利用文件的方式来抽象设备,所以驱动的编写也就是文件相关操作的编写,文件的结构体如下(内核版本5.13,下同):

struct file {
       union {
               struct llist_node       fu_llist;
               struct rcu_head         fu_rcuhead;
       } f_u;
       struct path             f_path;
       struct inode            *f_inode;       /* cached value */
       const struct file_operations    *f_op;

       /*
        * Protects f_ep, f_flags.
        * Must not be taken from IRQ context.
        */

   	enum rw_hint            f_write_hint;
       atomic_long_t           f_count;
       unsigned int            f_flags;
       fmode_t                 f_mode;
       struct mutex            f_pos_lock;
       loff_t                  f_pos;
       struct fown_struct      f_owner;
       const struct cred       *f_cred;
       struct file_ra_state    f_ra;

       u64                     f_version;
#ifdef CONFIG_SECURITY
       void                    *f_security;
#endif
       /* needed for tty driver, and maybe others */
       void                    *private_data;

#ifdef CONFIG_EPOLL
       /* Used by fs/eventpoll.c to link all the hooks to this file */
       struct hlist_head       *f_ep;
#endif /* #ifdef CONFIG_EPOLL */
       struct address_space    *f_mapping;
       errseq_t                f_wb_err;
       errseq_t                f_sb_err; /* for syncfs */
} __randomize_layout
 __attribute__((aligned(4)));  /* lest something weird decides that 2 is OK */

​ 由于现在只是写一个简单的驱动,所以很多东西不去深究,只看最主要的部分,我们要用到的其实只有struct file_operations *f_op void * private_data这两个成员。

​ file_operations在1.1.2有介绍,至于private_data是内核留给驱动程序员的一个空闲变量,如果想用就可以在写驱动时自己指定一个值,否则不用也没事。

1.1.1 Linux中的文件对象

​ 继续往下说之前,先要搞明白文件这个东西在Linux内存中到底是以什么结构来组织的,这个问题涉及到三个数据结构,一个是刚才的struct file,一个是sturct inode,还有一个是struct superblock

​ 后两个结构体就没必要列出来了,毕竟我们只要知道它们是干什么就行。

​ 这三个结构体的作用如下:

  • file:内存中的文件对象,也是用户对文件一切操作的入口和文件状态的记录者。
  • inode:文件数据的实际持有者。
  • superblock:inode信息的相关记录。

​ 这里就有一个值得注意的点:file中没有实际的文件数据,只有inode的指针

1.1.2 file_operations

​ 好了,接下里重点就是file_operations了,故名思意,就是文件操作的意思,里面的成员除了第一个是文件指针外,其余全都是文件操作的函数指针。

​ 那这和驱动程序又有什么关系呢?别忘了,驱动程序就是设备文件操作的集合,所以驱动程序需要实现的函数原型就在这里,毕竟你驱动要插进内核里,就得按照内核的规矩来办事。

​ 具体的file_operations定义如下:

struct file_operations {
        struct module *owner;
        loff_t (*llseek) (struct file *, loff_t, int);
        ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
        ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);
        ssize_t (*read_iter) (struct kiocb *, struct iov_iter *);
        ssize_t (*write_iter) (struct kiocb *, struct iov_iter *);
        int (*iopoll)(struct kiocb *kiocb, bool spin);
        int (*iterate) (struct file *, struct dir_context *);
        int (*iterate_shared) (struct file *, struct dir_context *);
        __poll_t (*poll) (struct file *, struct poll_table_struct *);
        long (*unlocked_ioctl) (struct file *, unsigned int, unsigned long);
        long (*compat_ioctl) (struct file *, unsigned int, unsigned long);
        int (*mmap) (struct file *, struct vm_area_struct *);
        unsigned long mmap_supported_flags;
        int (*open) (struct inode *, struct file *);
        int (*flush) (struct file *, fl_owner_t id);
        int (*release) (struct inode *, struct file *);
        int (*fsync) (struct file *, loff_t, loff_t, int datasync);
        int (*fasync) (int, struct file *, int);int (*lock) (struct file *, int, struct file_lock *);
	    int (*lock) (struct file *, int, struct file_lock *);
        ssize_t (*sendpage) (struct file *, struct page *, int, size_t, loff_t *, int);
        unsigned long (*get_unmapped_area)(struct file *, unsigned long, unsigned long, unsigned long, unsigned long);
        int (*check_flags)(int);
        int (*flock) (struct file *, int, struct file_lock *);
        ssize_t (*splice_write)(struct pipe_inode_info *, struct file *, loff_t *, size_t, unsigned int);
        ssize_t (*splice_read)(struct file *, loff_t *, struct pipe_inode_info *, size_t, unsigned int);
        int (*setlease)(struct file *, long, struct file_lock **, void **);
        long (*fallocate)(struct file *file, int mode, loff_t offset,
                          loff_t len);
        void (*show_fdinfo)(struct seq_file *m, struct file *f);
#ifndef CONFIG_MMU
        unsigned (*mmap_capabilities)(struct file *);
#endif
        ssize_t (*copy_file_range)(struct file *, loff_t, struct file *,
                        loff_t, size_t, unsigned int);
        loff_t (*remap_file_range)(struct file *file_in, loff_t pos_in,
                                   struct file *file_out, loff_t pos_out,
                                   loff_t len, unsigned int remap_flags);
        int (*fadvise)(struct file *, loff_t, loff_t, int);
} __randomize_layout;

​ 这么多methods,如果一次性介绍清楚也不现实,况且现在我也用不到,就简单说几个实现的程序中要用到的东西吧。

	/*控制文件指针(file中的f_pos)的偏移
	*para1:文件指针
	*para2:偏移量
	*para3:偏移基址,或者也可称为偏移方式,就是这个意思的东西
	*para3的取值如下:
	*#define SEEK_SET        0        seek relative to beginning of file 
	*#define SEEK_CUR        1        seek relative to current file position 
	*#define SEEK_END        2        seek relative to end of file 
	*返回值是偏移后的文件指针
	*/
	loff_t (*llseek) (struct file *, loff_t, int);
	//读取文件
 	ssize_t (*read) (struct file *, char __user *, size_t, loff_t *);
	//写入文件
    ssize_t (*write) (struct file *, const char __user *, size_t, loff_t *);

​ 不要忘了,文件只是系统给设备的一个抽象,所以上面的read和write函数实际的含义(对于驱动程序来讲),应该是下面这样:

  • read:读取设备返回的数据。
  • write:向设备发送命令或数据。

​ 关于驱动程序的介绍就到这里了,其实还留了一点,到底内核是怎么调用到驱动程序员定义的函数的呢?比方说我们在hello_user.c中fopen了一个hello设备,这个fopen到底怎么调用到file_operations中的open的呢?这个问题在下一小节中和设备文件一起说吧。

1.2 设备文件

​ 驱动程序说白了是一组操作的集合,而它要操作的实体便是设备文件,可以看到file_operations中的几乎每一个函数中会有一个struct file * ,就是通过这个参数把驱动程序要操作的设备文件对象传递进来的。

​ 当然,这个设备文件一样有inode以及管理其inode的superblock,不过这和我们目前要实现的一个简单的驱动没关系。

1.2.1 MAJOR与MINOR

​ 首先摆在内核眼前的一个问题就是,我们的计算机有各种各样的设备,各种各样的驱动程序,它要怎么区分这么多设备和驱动?又怎么能知道哪个设备应该使用哪个驱动?

​ 答案就是通过MAJOR(主设备号)与MINOR(次设备号)来管理。

​ MAJOR是用来标识驱动程序的一个12bit的无符号整数,MINOR是用来管理挂载设备的20bit无符号整数,对于两个设备文件来说,它们的MINOR能相同,但MAJOR和MINOR不能相同。

​ 这是因为如果一个设备MAJOR = 255,MINOR = 0,它代表的意思是使用255号驱动的第0号设备,另一个设备MAJOR = 256,MINOR = 0,它代表的意思是使用第256号驱动的0号设备。

​ 明显,MINOR即使相同,也能区分出设备,而两者如果都相同,自然就成了问题。

​ MAJOR和MINOR在kernel中的定义如下:

#define MINORBITS       20
#define MINORMASK       ((1U << MINORBITS) - 1)

#define MAJOR(dev)      ((unsigned int) ((dev) >> MINORBITS))
#define MINOR(dev)      ((unsigned int) ((dev) & MINORMASK))
#define MKDEV(ma,mi)    (((ma) << MINORBITS) | (mi))

​ 由此可见,MAJOR和MINOR本身就是一个32bit的整数拆成了两部分,前12bit是MAJOR,后20bit是MAJOR。

​ MAJOR一般来讲是由内核在驱动插入时动态的,不过注册MAJOR这个任务本身是由驱动程序来完成的,驱动程序员愿意的话也可以自己瞎指定一个MAJOR,只不过要是和当前已经挂载的驱动冲突的话。如果冲突的话,我也没试过也不知道会发生什么,总之不会是好事。

1.2.2 查看系统现在已有的设备文件

​ 当前系统所有挂载的设备都在/dev下,输入如下命令:

ls -al /dev

由于太多了,我这里截取了一部分:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uVXLuyl9-1673510161656)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1655124429332.png)]

黄色的就是设备文件,可以看到,其他文件显示大小的地方,设备文件显示的是两个数字,这就是MINOR,MAJOR。

​ 当前所有插入到内核中的驱动都可以在/proc/devices文件中查看,输入如下命令:

cat /proc/devices

同样只截取一部分:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4o3rKDh2-1673510161657)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1655124790430.png)]

module(驱动程序模块的意思)前的数字就是MAJOR。

1.2.3 设备的分类

​ 在查看/proc/devices文件时,会看到Character devices,Block devices的字样,这就是设备的分类,这两类设备的具体区别如下(摘自《Linux Devices Driver 3rd》第一章1.3节):

Character devices

A character (char) device is one that can be accessed as a stream of bytes

(like a file); a char driver is in charge of implementing this behavior. Such a

driver usually implements at least the open, close, read, and write system

calls. The text console (/dev/console) and the serial ports (/dev/ttyS0 and

friends) are examples of char devices, as they are well represented by the

stream abstraction. Char devices are accessed by means of filesystem nodes,

such as /dev/tty1 and /dev/lp0. The only relevant difference between a char

device and a regular file is that you can always move back and forth in the

regular file, whereas most char devices are just data channels, which you can

only access sequentially. There exist, nonetheless, char devices that look like

data areas, and you can move back and forth in them; for instance, this

usually applies to frame grabbers, where the applications can access the whole

acquired image using mmap or lseek.

Block devices

Like char devices, block devices are accessed by filesystem nodes in the /dev

directory. A block device is a device (e.g., a disk) that can host a filesystem. In

most Unix systems, a block device can only handle I/O operations that transfer

one or more whole blocks, which are usually 512 bytes (or a larger power of

two) bytes in length. Linux, instead, allows the application to read and write a

block device like a char device. it permits the transfer of any number of bytes

at a time. As a result, block and char devices differ only in the way data is

managed internally by the kernel, and thus in the kernel/driver software

interface. Like a char device, each block device is accessed through a

filesystem node, and the difference between them is transparent to the user.

Block drivers have a completely different interface to the kernel than char

drivers.

这段说的东西对我们首先目标已经够用了,我们要实现的HelloModule是一个字符型设备驱动。

1.2.4 文件打开的调用栈
1.事前准备

​ 之前就有说过,要介绍一下文件的打开过程,现在就是时候了,我们从open函数开始说起(glibc-2.35)。友情提示一下,这部分内容可能会比较难,而且也即使不懂也并不影响之后的编程,只不过是我的求知欲作祟而已,所以基础不太好的人选着看吧。

​ 首先是工具和源码的下载,这部分转到2.2.1节。

​ 在阅读阅读源码之前,要做的第一件事情一定是编译,许多由脚本生成的代码你不make一下是没有的,至于glibc和kernel的编译转到。

2.glibc的open(用户空间)

​ 我们跟踪glibc的open函数可以发现,open的定义有两处,一处是在fcntl.h中,这里明显只定义了函数原型,并没有实现,另一处是在loadmsgcat.c中,代码如下:

// intl/loadmsgcat.c
# define open(name, flags)      __open_nocancel (name, flags)

继续跟踪:

// sysdeps/unix/sysv/linux/open64_nocancel.c
int
__open64_nocancel (const char *file, int oflag, ...)
{
  int mode = 0;

  if (__OPEN_NEEDS_MODE (oflag))
    {
      va_list arg;
      va_start (arg, oflag);
      mode = va_arg (arg, int);
      va_end (arg);
    }

  return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag | O_LARGEFILE,
                              mode);
}

注意:这里我想看到的涉及到体系结构依赖的都要选择sysv/linux目录下的文件,不要一会这个体系结构一会那个体系结构。

// sysdeps/unix/sysdep.h

//1
#define INLINE_SYSCALL_CALL(...) \
  __INLINE_SYSCALL_DISP (__INLINE_SYSCALL, __VA_ARGS__)

//2
#define __INLINE_SYSCALL_DISP(b,...) \
  __SYSCALL_CONCAT (b,__INLINE_SYSCALL_NARGS(__VA_ARGS__))(__VA_ARGS__)

//4
#define __INTERNAL_SYSCALL_NARGS_X(a,b,c,d,e,f,g,h,n,...) n
//3
#define __INTERNAL_SYSCALL_NARGS(...) \
  __INTERNAL_SYSCALL_NARGS_X (__VA_ARGS__,7,6,5,4,3,2,1,0,)

//6
#define __SYSCALL_CONCAT_X(a,b)     a##b
//5
#define __SYSCALL_CONCAT(a,b)       __SYSCALL_CONCAT_X (a, b)

//7
#define __INTERNAL_SYSCALL0(name) \
  INTERNAL_SYSCALL (name, 0)
#define __INTERNAL_SYSCALL1(name, a1) \
  INTERNAL_SYSCALL (name, 1, a1)
#define __INTERNAL_SYSCALL2(name, a1, a2) \
  INTERNAL_SYSCALL (name, 2, a1, a2)
#define __INTERNAL_SYSCALL3(name, a1, a2, a3) \
  INTERNAL_SYSCALL (name, 3, a1, a2, a3)
#define __INTERNAL_SYSCALL4(name, a1, a2, a3, a4) \
  INTERNAL_SYSCALL (name, 4, a1, a2, a3, a4)
#define __INTERNAL_SYSCALL5(name, a1, a2, a3, a4, a5) \
  INTERNAL_SYSCALL (name, 5, a1, a2, a3, a4, a5)
#define __INTERNAL_SYSCALL6(name, a1, a2, a3, a4, a5, a6) \
  INTERNAL_SYSCALL (name, 6, a1, a2, a3, a4, a5, a6)
#define __INTERNAL_SYSCALL7(name, a1, a2, a3, a4, a5, a6, a7) \
  INTERNAL_SYSCALL (name, 7, a1, a2, a3, a4, a5, a6, a7)

​ 好了,到这里停一下,不知道有没有像我一样第一次差点跟丢了。上面一连串的宏其实在文件中的顺序,应该是和我这里列出来的相反,我是一步一步跟着来的,这样看起来理解起来也比较方便一些。

​ 跟着我上面的序号来一遍,这也是这些宏的调用顺序。

​ 1号宏这里将__INLINE_SYSCALL这个字符串以及传递过来的可变参数传递给了2号宏,也就是__INLINE_SYSCALL_DISP,2号宏调用了5号宏(__SYSCALL_CONCAT),但是在5号宏中还有一个3号宏(__INLINE_SYSCALL_NARGS),先执行的是3号宏。

​ 接下来就是比较难的地方了——3号宏和4号宏的作用。看序号也知道,最后是要调用7号底下那一堆宏中的某一个,7号宏的名字和1号宏传递给2号宏的字符串只差最后一个数字字符,只要有了那个数字字符,2号宏调用的5号宏就能将__INLINE_SYSCALL与其连接起来,2号宏实质上就变成了7号中某一个宏的调用

​ 那个数字字符的意义是系统调用的参数个数(除系统调用号以外),看下面这行代码:

 return INLINE_SYSCALL_CALL (openat, AT_FDCWD, file, oflag | O_LARGEFILE,
                              mode);

在最开始1号宏被调用时,第一个参数是系统调用的名字,它随后会被翻译成系统调用号,后面还有4个参数,所以最终会调用__ INTERNAL_SYSCALL4。

​ 3,4号宏的作用就是产生这个数字字符。

​ 3号宏把上面5个参数都传递给了4号宏,4号宏的返回值始终是n,也就是传递过来的第9个参数,现在传递给4号宏第九个参数是什么需要看3号宏的调用,__VA_ARGS__中有5个参数,所以第六个参数是’7’,第七个参数是’6’,第八个参数是’5’,第九个参数是’4’,由此4号宏的返回值是4。确定了__VA_ARGS__中除了第一个参数(系统调用名称)外还有四个参数。

​ 不知道大家怎么想,可能是我见得少吧,我当时看懂了以后觉得想出这个办法的人真的聪明。

​ 好了,我们继续跟踪__ INTERNAL_SYSCALL4:

// sysdeps/unix/sysv/linux/x86_64/sysdep.h

#undef INTERNAL_SYSCALL
#define INTERNAL_SYSCALL(name, nr, args...)                             \
        internal_syscall##nr (SYS_ify (name), args)

#undef SYS_ify
#define SYS_ify(syscall_name)   __NR_##syscall_name

#undef internal_syscall4
#define internal_syscall4(number, arg1, arg2, arg3, arg4)               \
({                                                                      \
    unsigned long int resultvar;                                        \
    TYPEFY (arg4, __arg4) = ARGIFY (arg4);                              \
    TYPEFY (arg3, __arg3) = ARGIFY (arg3);                              \
    TYPEFY (arg2, __arg2) = ARGIFY (arg2);                              \
    TYPEFY (arg1, __arg1) = ARGIFY (arg1);                              \
    register TYPEFY (arg4, _a4) asm ("r10") = __arg4;                   \
    register TYPEFY (arg3, _a3) asm ("rdx") = __arg3;                   \
    register TYPEFY (arg2, _a2) asm ("rsi") = __arg2;                   \
    register TYPEFY (arg1, _a1) asm ("rdi") = __arg1;                   \
    asm volatile (                                                      \
    "syscall\n\t"                                                       \
    : "=a" (resultvar)                                                  \
    : "0" (number), "r" (_a1), "r" (_a2), "r" (_a3), "r" (_a4)          \
    : "memory", REGISTERS_CLOBBERED_BY_SYSCALL);                        \
    (long int) resultvar;                                               \
})


INTERNAL_SYSCALL每个平台的实现都不一样,这里我选择的x86_64平台,单纯是因为我对x86的汇编比较熟悉。

上面的代码用的把戏和之前没有太大区别,就不多赘述了,最后会调用internal_syscall4,使用内联汇编来执行系统调用。

最后需要清楚系统调用号,方便一会查看内核的代码,如下:

#define __NR_openat 56

关于glibc更具体的讲解可以看看这位大佬的文章:

https://www.zhihu.com/column/c_144857088

3.sys_openat系统调用(内核空间)

​ 这里体系结构一样采用的是x86_64。

​ glibc通过软中断的方式进入了内核,中断号是0x80,内核会根据IDT去执行相应的函数:

/*	arch/x86/entry/entry_64.S */

SYM_CODE_START(entry_SYSCALL_64)
        UNWIND_HINT_EMPTY

        swapgs
        /* tss.sp2 is scratch space. */
        movq    %rsp, PER_CPU_VAR(cpu_tss_rw + TSS_sp2)
        SWITCH_TO_KERNEL_CR3 scratch_reg=%rsp
        movq    PER_CPU_VAR(cpu_current_top_of_stack), %rsp

SYM_INNER_LABEL(entry_SYSCALL_64_safe_stack, SYM_L_GLOBAL)

        /* Construct struct pt_regs on stack */
        pushq   $__USER_DS                              /* pt_regs->ss */
        pushq   PER_CPU_VAR(cpu_tss_rw + TSS_sp2)       /* pt_regs->sp */
        pushq   %r11                                    /* pt_regs->flags */
        pushq   $__USER_CS                              /* pt_regs->cs */
        pushq   %rcx                                    /* pt_regs->ip */
SYM_INNER_LABEL(entry_SYSCALL_64_after_hwframe, SYM_L_GLOBAL)
        pushq   %rax                                    /* pt_regs->orig_ax */

        PUSH_AND_CLEAR_REGS rax=$-ENOSYS

        /* IRQs are off. */
        movq    %rax, %rdi
        movq    %rsp, %rsi
        call    do_syscall_64           /* returns with IRQs disabled */

​ 代码并没有截全,不用关注其他地方,直接看do_syscall_64函数:

// arch/x86/entry/common.c

#ifdef CONFIG_X86_64
__visible noinstr void do_syscall_64(unsigned long nr, struct pt_regs *regs)
{
        add_random_kstack_offset();
        nr = syscall_enter_from_user_mode(regs, nr);

        instrumentation_begin();
        if (likely(nr < NR_syscalls)) {
                nr = array_index_nospec(nr, NR_syscalls);
                regs->ax = sys_call_table[nr](regs);
#ifdef CONFIG_X86_X32_ABI
        } else if (likely((nr & __X32_SYSCALL_BIT) &&
                          (nr & ~__X32_SYSCALL_BIT) < X32_NR_syscalls)) {
                nr = array_index_nospec(nr & ~__X32_SYSCALL_BIT,
                                        X32_NR_syscalls);
            //重点在下面这一行,rax作为返回值,调用系统调用表中的函数
                regs->ax = x32_sys_call_table[nr](regs);
#endif
        }
        instrumentation_end();
        syscall_exit_to_user_mode(regs);
}
#endif

​ 有关系统调用表和中断调用表的内容就不多说了,这里曾经有一点让我困惑的地方,为什么要把regs作为参数传递过去?在《Understanding Linux Kernel 3rd》中我得到了答案,Linux将系统调用的参数写到了寄存器中,而不是再去用户栈copy到内核栈,所以实际上就是regs就是系统调用的参数。

​ kernel中系统调用的名字统一都是sys_xyz()的格式,xyz代指具体的系统调用名称,现在要去找sys_openat:

//	fs/open.c

SYSCALL_DEFINE4(openat, int, dfd, const char __user *, filename, int, flags,
                umode_t, mode)
{
        if (force_o_largefile())
                flags |= O_LARGEFILE;
        return do_sys_open(dfd, filename, flags, mode);
}

​ 这个其实就是sys_openat的定义,kernel为了防止系统调用返回值被编译器优化掉,采用了一系列宏来实现定义,具体就不展开说了。

//	fs/open.c

long do_sys_open(int dfd, const char __user *filename, int flags, umode_t mode)
{
        struct open_how how = build_open_how(flags, mode);
    	///
        return do_sys_openat2(dfd, filename, &how);
    	///
}

static long do_sys_openat2(int dfd, const char __user *filename,
                           struct open_how *how)
{
        struct open_flags op;
        int fd = build_open_flags(how, &op);
        struct filename *tmp;

        if (fd)
                return fd;

        tmp = getname(filename);
        if (IS_ERR(tmp))
                return PTR_ERR(tmp);

        fd = get_unused_fd_flags(how->flags);
        if (fd >= 0) {
            	///
                struct file *f = do_filp_open(dfd, tmp, &op);
            	///
                if (IS_ERR(f)) {
                        put_unused_fd(fd);
                        fd = PTR_ERR(f);
                } else {
                        fsnotify_open(f);
                        fd_install(fd, f);
                }
        }
        putname(tmp);
        return fd;
}


//	fs/namei.c
struct file *do_filp_open(int dfd, struct filename *pathname,
                const struct open_flags *op)
{
        struct nameidata nd;
        int flags = op->lookup_flags;
        struct file *filp;

        set_nameidata(&nd, dfd, pathname);
    	///
        filp = path_openat(&nd, op, flags | LOOKUP_RCU);
    	///
        if (unlikely(filp == ERR_PTR(-ECHILD)))
                filp = path_openat(&nd, op, flags);
        if (unlikely(filp == ERR_PTR(-ESTALE)))
                filp = path_openat(&nd, op, flags | LOOKUP_REVAL);
        restore_nameidata();
        return filp;
}

static struct file *path_openat(struct nameidata *nd,
                        const struct open_flags *op, unsigned flags)
{
        struct file *file;
        int error;

        file = alloc_empty_file(op->open_flag, current_cred());
        if (IS_ERR(file))
                return file;

        if (unlikely(file->f_flags & __O_TMPFILE)) {
                error = do_tmpfile(nd, flags, op, file);
        } else if (unlikely(file->f_flags & O_PATH)) {
                error = do_o_path(nd, flags, file);
        } else {
                const char *s = path_init(nd, flags);
                while (!(error = link_path_walk(s, nd)) &&
                       (s = open_last_lookups(nd, file, op)) != NULL)
                        ;
                if (!error)
                    	///
                        error = do_open(nd, file, op);
            			///
                terminate_walk(nd);
        }
        if (likely(!error)) {
                if (likely(file->f_mode & FMODE_OPENED))
                        return file;
                WARN_ON(1);
                error = -EINVAL;
        }
        fput(file);
        if (error == -EOPENSTALE) {
                if (flags & LOOKUP_RCU)
                        error = -ECHILD;
                else
                        error = -ESTALE;
        }
        return ERR_PTR(error);
}

static int do_open(struct nameidata *nd,
                   struct file *file, const struct open_flags *op)
{
        struct user_namespace *mnt_userns;
        int open_flag = op->open_flag;
        bool do_truncate;
        int acc_mode;
        int error;

        if (!(file->f_mode & (FMODE_OPENED | FMODE_CREATED))) {
                error = complete_walk(nd);
                if (error)
                        return error;
        }
        if (!(file->f_mode & FMODE_CREATED))       
            audit_inode(nd->name, nd->path.dentry, 0);
        mnt_userns = mnt_user_ns(nd->path.mnt);
        if (open_flag & O_CREAT) {
                if ((open_flag & O_EXCL) && !(file->f_mode & FMODE_CREATED))
                        return -EEXIST;
                if (d_is_dir(nd->path.dentry))
                        return -EISDIR;
                error = may_create_in_sticky(mnt_userns, nd,
                                             d_backing_inode(nd->path.dentry));
                if (unlikely(error))
                        return error;
        }
        if ((nd->flags & LOOKUP_DIRECTORY) && !d_can_lookup(nd->path.dentry))
                return -ENOTDIR;

        do_truncate = false;
        acc_mode = op->acc_mode;
        if (file->f_mode & FMODE_CREATED) {
                /* Don't check for write permission, don't truncate */
                open_flag &= ~O_TRUNC;
        	 acc_mode = 0;
        } else if (d_is_reg(nd->path.dentry) && open_flag & O_TRUNC) {
                error = mnt_want_write(nd->path.mnt);
                if (error)
                        return error;
                do_truncate = true;
        }
        error = may_open(mnt_userns, &nd->path, acc_mode, open_flag);
        if (!error && !(file->f_mode & FMODE_OPENED))
            ///
                error = vfs_open(&nd->path, file);
    		//
        if (!error)
                error = ima_file_check(file, op->acc_mode);
        if (!error && do_truncate)
                error = handle_truncate(mnt_userns, file);
        if (unlikely(error > 0)) {
                WARN_ON(1);
                error = -EINVAL;
        }
        if (do_truncate)
                mnt_drop_write(nd->path.mnt);
        return error;
}

//	fs/open.c

int vfs_open(const struct path *path, struct file *file)
{
        file->f_path = *path;
    	///
        return do_dentry_open(file, d_backing_inode(path->dentry), NULL);
    	//
}

static int do_dentry_open(struct file *f,
                          struct inode *inode,
                          int (*open)(struct inode *, struct file *))
{
        static const struct file_operations empty_fops = {};
        int error;

        path_get(&f->f_path);
        f->f_inode = inode;
        f->f_mapping = inode->i_mapping;
        f->f_wb_err = filemap_sample_wb_err(f->f_mapping);
        f->f_sb_err = file_sample_sb_err(f);

        if (unlikely(f->f_flags & O_PATH)) {
                f->f_mode = FMODE_PATH | FMODE_OPENED;
                f->f_op = &empty_fops;
                return 0;
        }

        if (f->f_mode & FMODE_WRITE && !special_file(inode->i_mode)) {
                error = get_write_access(inode);
				if (unlikely(error))
                        goto cleanup_file;
                error = __mnt_want_write(f->f_path.mnt);
                if (unlikely(error)) {
                        put_write_access(inode);
                        goto cleanup_file;
                }
                f->f_mode |= FMODE_WRITER;
        }

        /* POSIX.1-2008/SUSv4 Section XSI 2.9.7 */
        if (S_ISREG(inode->i_mode) || S_ISDIR(inode->i_mode))
                f->f_mode |= FMODE_ATOMIC_POS;

    	
        f->f_op = fops_get(inode->i_fop);
    	/
        if (WARN_ON(!f->f_op)) {
                error = -ENODEV;
                goto cleanup_all;
        }
	 error = security_file_open(f);
        if (error)
                goto cleanup_all;

        error = break_lease(locks_inode(f), f->f_flags);
        if (error)
                goto cleanup_all;

        /* normally all 3 are set; ->open() can clear them if needed */
        f->f_mode |= FMODE_LSEEK | FMODE_PREAD | FMODE_PWRITE;
        if (!open)
            	
                open = f->f_op->open;
    			
        if (open) {
            	///
                error = open(inode, f);
            	//
                if (error)
                        goto cleanup_all;
        }
        f->f_mode |= FMODE_OPENED;
        if ((f->f_mode & (FMODE_READ | FMODE_WRITE)) == FMODE_READ)
                i_readcount_inc(inode);
        if ((f->f_mode & FMODE_READ) &&
             likely(f->f_op->read || f->f_op->read_iter))
                f->f_mode |= FMODE_CAN_READ;
        if ((f->f_mode & FMODE_WRITE) &&
             likely(f->f_op->write || f->f_op->write_iter))
                f->f_mode |= FMODE_CAN_WRITE;

        f->f_write_hint = WRITE_LIFE_NOT_SET;
        f->f_flags &= ~(O_CREAT | O_EXCL | O_NOCTTY | O_TRUNC);

        file_ra_state_init(&f->f_ra, f->f_mapping->host->i_mapping);

        /* NB: we're sure to have correct a_ops only after f_op->open */
        if (f->f_flags & O_DIRECT) {
                if (!f->f_mapping->a_ops || !f->f_mapping->a_ops->direct_IO)
                        return -EINVAL;
        }
        /*
         * XXX: Huge page cache doesn't support writing yet. Drop all page
         * cache for this file before processing writes.
         */
        if ((f->f_mode & FMODE_WRITE) && filemap_nr_thps(inode->i_mapping))
                truncate_pagecache(inode, 0);

        return 0;

cleanup_all:
        if (WARN_ON_ONCE(error > 0))
                error = -EINVAL;
        fops_put(f->f_op);
        if (f->f_mode & FMODE_WRITER) {
                put_write_access(inode);
                __mnt_drop_write(f->f_path.mnt);
        }
cleanup_file:
        path_put(&f->f_path);
        f->f_path.mnt = NULL;
        f->f_path.dentry = NULL;
        f->f_inode = NULL;
        return error;
}

代码很长,但很多错误检查不看,只看核心调用也没多少,跟着我上面用注释分隔出来的调用,可以发现这一长串的调用最后执行的是f->fp->open。

二 基本流程与工具配置

2.1 基本流程

​ 其实实际上,通过第一部分的学习,已经把基本流程说了一遍,现在只需要添加一点点新东西。

​ 采用自顶向下的方式去看要完成的任务:编写一个hello驱动,将其加载进内核,创建hello_dev,把一个字符串正序写入hello_dev,再逆序读出来,输出读取结果。

​ 即使驱动和设备文件都已经有了,没有调用驱动去操作设备文件的顶层应用也是白搭,所以第一件事情就是编写一个调用驱动程序操作设备文件的c程序,如下:

//hello_user.c

#include <stdio.h>

int main(){
	FILE * hello = fopen("/dev/hello_dev","r+");
	char data[] = {'1','2','3','4','5'};
	fwrite(data,1,5,hello);
	
	char buf[5] = {0,0,0,0,0};
	int buffer_pos = 0,cur_pos = 4;
	while(cur_pos >= 0){
		int res = fseek(hello,cur_pos,SEEK_SET);
		fread(buf + buffer_pos,1,1,hello);
		++buffer_pos;
		--cur_pos;
	}
	
	buffer_pos = 0;
	for(;buffer_pos < 5;buffer_pos++)
		printf("%c",buf[buffer_pos]);
	fclose(hello);
	return 0;
}

这个程序很简单,就是利用最基本的文件读写来实现要实现的功能。

***第二件事情,就是编写驱动程序,然后编译生成hello.ko。***这一部分将第三章详细说明。

三,利用insmod 将hello.ko插入内核。

四,在/dev/下mknod创建设备文件:hello_dev。

五,编译hello_user并运行。

这一节只是一个大纲,让大家知道接下来要干些什么,不要只是跟着各种各样的资料敲了两下键盘,脑海中没有一个完整的流程。

2.2 环境与工具配置

2.2.1 源码阅读工具 global

​ 安装的话无非apt-get install,不多赘述。

​ 安装完毕后,到想要阅读的项目根目录下执行以下命令:

gtags -v

​ 我个人习惯在vim中使用global,当然,需要在vim中安装相应的插件。

First, do the preparation of global. See Section 2.1 [Preparation], page 3.

Second, copy gtags.vim to your plug-in directory or source it from your vimrc.

$ cp /usr/local/share/gtags/gtags.vim $HOME/.vim/plugin

​ 以上内容来自global的官方手册。在vim中global常用的操作有:

To go to main, you can say

:Gtags main

Vim executes global(1), parses the output, lists located tags in quickfix window and

loads the first entry. The quickfix window is like this:

gozilla/gozilla.c|200| main(int argc, char **argv)

gtags-cscope/gtags-cscope.c|124| main(int argc, char **argv)

gtags-parser/asm_scan.c|2056| int main()

gtags-parser/gctags.c|157| main(int argc, char **argv)

gtags-parser/php.c|2116| int main()

gtags/gtags.c|152| main(int argc, char **argv)

[Quickfix List]

You can go to any entry using quickfix command.

:cn go to the next entry.

:cp go to the previous entry.

:ccN go to the N’th entry.

:cl list all entries.

The GtagsCursor command brings you to the definition or reference of the current

token.

If the context of the current token is a definition then it is equivalent to :Gtags -r

current-token; if it is a reference to some definitions then it is equivalent to :Gtags

current-token; else it is equivalent to :Gtags -s current-token.

:GtagsCursor

Suggested map:

map <C->^] :GtagsCursor

Though the mapping of :GtagsCursor to ^] seems suitable, it will bring an inconve

nience in the help screen.

2.2.2 buildroot与qmenu

​ 之所以要用到这两个东西,是为了编译内核,其实直接编译内核也可以,但是你编译完的内核总得有个硬件环境和根文件系统吧,buildroot和qmenu就解决了这两个问题。

​ 有些人为了图省事,直接在现在的虚拟机上insmod,有可能会插不进去。因为对于内核来说,驱动属于外来程序,内核对其并不是充分信任的,只有在编译内核时明确指定允许未认证的内核插入,才能顺利挂载驱动。

​ 此外,编译驱动程序生成的.ko文件依赖于特定的内核版本,一个版本的驱动不能在另一个版本的内核上运行。

​ 驱动插入内核后,如果编写不当,很有可能造成各种各样的问题,这也是另一个不建议在现在的虚拟机上直接编译并插入内核的原因,如果要是直接把内核搞废了,这个虚拟机里面的东西就都丢了。

1. buildroot

​ 源码到官网上下载就可以,这里说一下buildroot的配置。

​ 首先执行make menuconfig。需要修改和注意的地方有两个,一个是kernel中的配置,一个是libc的选择。

​ 下图是kernel的配置:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WKcuQ9M6-1673510161658)(C:\Users\admin\AppData\Roaming\Typora\typora-user-images\1656318785335.png)]

​ Kernel version选择Custom tarball,这样就能编译已经准备好的本地内核源码。

​ 第三行填写源码压缩包的URL,这里就指定本地的路径即可。

​ 第五行指定编译内核时的配置文件(.config),这个文件是需要在内核源码目录下执行make menuconfig来生成的。接下来说一下内核配置应该注意的内容,也就是内核的.config文件。

​ 到内核源码目录下执行make menuconfig,如果你明确知道自己想要编译什么,不想编译什么,就按照自己的理解去选择。如果你和我一样大部分内容都看不懂,就全按照默认的来,虽然这样选的多,编译出来的内核会很大,编译也会费时,但总好过出错。

​ 关闭内核的menuconfig并保存,就会出现.config文件,打开该文件,找到CONFIG_MODULE_SIG,将其置为n,如果 CONFIG_MODULE_SIG_FORCE 是y,也要置为n。

​ 到了这里,再继续返回buildroot的menuconfig,Kernel configure输入刚才生成的.config的路径。

​ 内核的配置就完成了,接下来是libc的选择:Toolchain -> C library -> glibc。

​ 之所以要选择glibc,是因为之前的那个hello_user.c使用gcc编译的,生成的hello_user.o也是依赖gnu的C库,将来要把这个文件安装到编译好的机子上运行,这台机子自然需要glibc。

​ 配置完后,make -j8,接下来是漫长的等待。编译内核的过程中也会遇到各种各样的问题,这里就不一一列举了。

2. qmenu

​ qmenu的话直接apt-get install 安装即可,然后在buildroot编译完后,执行以下命令:

sudo qemu-system-x86_64 -kernel buildroot目录/output/images/bzImage -boot c -m 2049M -hda buildroot目录/output/images/rootfs.ext2 -append “root=/dev/sda rw console=ttyS0,115200 acpi=off nokaslr” -serial stdio -display none

​ 如果没有出错的话,应该会出现类似的画面:

三 驱动程序编写:hello.c

​ 过了这么长时间,终于能够正儿八经的编写驱动程序了。

​ 在动手写之前,要有两个问题要决定一下:

  • 这个驱动想要操作的设备是什么样的,即要操作的数据结构。
  • 这个驱动要支持一些什么样的操作,即要实现的方法。

3.1 数据结构与方法

​ 我定义的hello_dev结构如下:

struct hello_dev{
    //设备数据区
	char data[80];
    //设备数据区的大小
	size_t size;
    //字符型设备结构体
	struct cdev cdev;
};

cdev这个结构体是kernel中定义的,每一个字符型设备都要定义这个结构体。

​ 最终要实现的目标是顺序写入,逆序读出,需要定义的方法如下:

static struct file_operations hello_op = 
{
	.owner = THIS_MODULE,
	.read = hello_read,
	.write = hello_write,
	.open = hello_open,
	.llseek = hello_seek,
	.release = hello_release,
};

驱动的主要内容就是这几个方法的实现,不过再次之前还有两个非常重要的函数。

3.2 __init与__exit函数

​ 驱动时通过insmod来挂载到内核的,那如何指定它在加载入内核的时候应该干些什么呢(例如获取所需资源等必要操作)?同理,如何指定它被内核拆卸时应该做些什么呢(例如释放占有的资源等必要操作)?答案就是通过__init与__exit函数。

​ 一些全局变量的定义:

#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/module.h>
#define HELLO_NAME "HelloMoudle"
#define DEVCNT 1
#define DEVSIZE 80
#define READ_ERR -1

MODULE_LICENSE("Dual BSD/GPL");
int hello_major = 0,hello_minor = 0;
dev_t dev;

​ __init和__exit函数定义如下,我写的可能复杂了一些,建议搭配LDD去看。

static int __init hello_init(void){
	int result;
	if(hello_major){
        //如果MAJOR已经被分配,从hello_minor开始依次注册DEVCNT个设备
		dev = MKDEV(hello_major,hello_minor);
		result = register_chrdev_region(dev,DEVCNT,HELLO_NAME);
	}
	else {
        //否则动态动态分配MAJOR并进行和上述if一样的操作
		result = alloc_chrdev_region(&dev,hello_minor,DEVCNT,HELLO_NAME);
		hello_major = MAJOR(dev);
	}
	if(result){
		printk("cann't get major %d\n",hello_major);
		return result;
	}
	
    //为设备数组分配相应的内存
	hello_dev = kmalloc(DEVCNT * sizeof(hello_dev),GFP_KERNEL);
	if(!hello_dev) printk(KERN_NOTICE "cann't get memory");
	int i,err = 0;
    //逐个初始化设备
	for(i = 0;i < DEVCNT;i++){
		err = 0;
       	//字符设备数据结构初始化
		cdev_init(&hello_dev[i].cdev,&hello_op);
		hello_dev[i].cdev.owner = THIS_MODULE;
        //将设备文件操作和驱动程序相关联
		hello_dev[i].cdev.ops = &hello_op;
         hello_dev[i].size = DEVSIZE;
		if(err)	
			printk(KERN_NOTICE "Error %d adding hello%d", err, i);
	
	}
	return 0;
}

static void __exit hello_cleanup(void){
	kfree(hello_dev);
}

3.3 文件操作函数实现

static int hello_open(struct inode * f_inode,struct file * fp){
	struct hello_dev * dev;
   	/*
   	*首先要获取当前设备的指针,但传递过来的参数中并没有
   	*f_inode->i_cdev指向了hello_dev结构体中的cdev成员,
   	*通过cantainer_of反向定位到cdev所在的hello_dev结构体
   	*/
	dev = container_of(f_inode->i_cdev,struct hello_dev,cdev);
	dev->size = DEVSIZE;
    //将要操作的dev指针放到private_data,以后每次操作该设备就不用像上面一样折腾了
	fp->private_data = dev;
	printk (KERN_NOTICE "hello_dev%d open!",(dev - hello_dev) / sizeof (struct hello_dev));
	return 0;
}

static int hello_release(struct inode * inode,struct file * fp){
	struct hello_dev * dev = fp->private_data;
	printk (KERN_NOTICE "hello_dev%d release!",(dev - hello_dev) / sizeof (struct hello_dev));
	return 0;
}

static ssize_t hello_read (struct file * fp,char __user * ubuf,size_t count,loff_t * f_ops){
	struct hello_dev * dev = fp->private_data;	
	int retval = 0;
	if(*f_ops >= dev->size) 
		goto out;
    //复制要读的数据到用户空间
	if(copy_to_user(ubuf,dev->data + (*f_ops),count)){
		retval = READ_ERR;
		goto out;
	}
	*f_ops += count;
	retval = count;
out:
	return retval; 
}

static ssize_t hello_write(struct file *fp,const char __user * ubuf,size_t count, loff_t * f_ops){
	struct hello_dev * dev = fp->private_data;
	int retval = 0;
	if(*f_ops >= dev->size) 
		goto out;
    //从用户空间复制数据到内核
	if(copy_from_user(dev->data + (*f_ops),ubuf,count)){
		retval = READ_ERR;
		goto out;
	}
	*f_ops += count;
	retval = count;
out:
	return retval; 
}

static loff_t hello_seek(struct file * fp,loff_t offset,int mode){
	struct hello_dev * dev = fp->private_data;
	loff_t newpos=0;
    	switch(mode){
        	case SEEK_SET: {   //SEEK_SET代表以文件头为偏移起始值
        		newpos=offset;
        		break;
        	}
        	case SEEK_CUR: {   //SEEK_CUP代表以当前位置为偏移起始值
        		newpos=fp->f_pos+offset;
        		break;
        	}
        	case SEEK_END:{    //SEEK_END代表以文件结尾为偏移起始值
        		newpos=DEVSIZE+offset;
        		break;
        	}
        	default:
        	    return -1;
    	}
    	if(newpos<0 || newpos >= dev->size)
        	return -1;
    	fp->f_pos = newpos;
    	return newpos;
}

需要注意的是,这些函数的原型必须按照file_operations中定义的函数指针来写。

3.4 Makefile

​ 到现在,驱动已经编写完了,接下来的工作就是编译驱动生成.ko文件。驱动的编译并不同于普通程序的编译,从include中的头文件也可以看出,驱动是完全依赖于内核的,其编译也不例外。

​ 内核的编译系统是一个庞大而复制的系统,我没有去深挖的意思,只需要知道内核为我们准备了哪些方法去编译驱动就行。

​ Makefile的编写如下:

ifeq ($(KERNELRELEASE),)
 KERNELDIR = #内核源码目录
 PWD = $(shell pwd)

modules:
	$(MAKE) -C $(KERNELDIR) M=$(PWD) modules
	
clean:
	rm -rf *.o *~core.depend .*.cmd *.ko *.mod.c .tmp_versions
else
 obj-m := hello.o
 
endif

这个Makefile其实会被执行两次,第一次检测到KERNELRELEASE没有定义,然后进入了KERNELDIR指定的目录,调用内核编译系统。

​ 内核编译系统回调上面的Makefile,发现KERNELRELEASE已经定义(这个变量在内核编译系统中被定义),通过obj-m知道了要根据hello.o生成驱动,然后生成了hello.ko。

完整代码如下:

#include <linux/types.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kernel.h>
#include <linux/uaccess.h>
#include <linux/slab.h>
#include <linux/module.h>
#define HELLO_NAME "HelloMoudle"
#define DEVCNT 1
#define DEVSIZE 80
#define READ_ERR -1

MODULE_LICENSE("Dual BSD/GPL");
int hello_major = 0,hello_minor = 0;
dev_t dev;

struct hello_dev{
	char data[80];
	size_t size;
	struct cdev cdev;
};
struct hello_dev * hello_dev = NULL;

static int hello_open(struct inode * f_inode,struct file * fp){
	struct hello_dev * dev;
	dev = container_of(f_inode->i_cdev,struct hello_dev,cdev);
	dev->size = DEVSIZE;
	fp->private_data = dev;
	printk (KERN_NOTICE "hello_dev%d open!",(dev - hello_dev) / sizeof (struct hello_dev));
	return 0;
}


static ssize_t hello_read (struct file * fp,char __user * ubuf,size_t count,loff_t * f_ops){
	struct hello_dev * dev = fp->private_data;	
	int retval = 0;
	if(*f_ops >= dev->size) 
		goto out;
	if(copy_to_user(ubuf,dev->data + (*f_ops),count)){
		retval = READ_ERR;
		goto out;
	}
	*f_ops += count;
	retval = count;
out:
	return retval; 
}

static ssize_t hello_write(struct file *fp,const char __user * ubuf,size_t count, loff_t * f_ops){
	struct hello_dev * dev = fp->private_data;
	int retval = 0;
	if(*f_ops >= dev->size) 
		goto out;
	if(copy_from_user(dev->data + (*f_ops),ubuf,count)){
		retval = READ_ERR;
		goto out;
	}
	*f_ops += count;
	retval = count;
out:
	return retval; 
}

static loff_t hello_seek(struct file * fp,loff_t offset,int mode){
	struct hello_dev * dev = fp->private_data;
	loff_t newpos=0;
    	switch(mode){
        	case SEEK_SET: {   //SEEK_SET代表以文件头为偏移起始值
        		newpos=offset;
        		break;
        	}
        	case SEEK_CUR: {   //SEEK_CUP代表以当前位置为偏移起始值
        		newpos=fp->f_pos+offset;
        		break;
        	}
        	case SEEK_END:{    //SEEK_END代表以文件结尾为偏移起始值
        		newpos=DEVSIZE+offset;
        		break;
        	}
        	default:
        	    return -1;
    	}
    	if(newpos<0 || newpos >= dev->size)
        	return -1;
    	fp->f_pos = newpos;
    	return newpos;
}

static int hello_release(struct inode * inode,struct file * fp){
	struct hello_dev * dev = fp->private_data;
	printk (KERN_NOTICE "hello_dev%d release!",(dev - hello_dev) / sizeof (struct hello_dev));
	return 0;
}

static struct file_operations hello_op = 
{
	.owner = THIS_MODULE,
	.read = hello_read,
	.write = hello_write,
	.open = hello_open,
	.llseek = hello_seek,
	.release = hello_release,
};


static int __init hello_init(void){
	int result;
	if(hello_major){
		dev = MKDEV(hello_major,hello_minor);
		result = register_chrdev_region(dev,DEVCNT,HELLO_NAME);
	}
	else {
		result = alloc_chrdev_region(&dev,hello_minor,DEVCNT,HELLO_NAME);
		hello_major = MAJOR(dev);
	}
	if(result){
		printk("cann't get major %d\n",hello_major);
		return result;
	}
	
	hello_dev = kmalloc(DEVCNT * sizeof(hello_dev),GFP_KERNEL);
	if(!hello_dev) printk(KERN_NOTICE "cann't get memory");
	int i,err = 0;
	for(i = 0;i < DEVCNT;i++){
		err = 0;
		cdev_init(&hello_dev[i].cdev,&hello_op);
		hello_dev[i].cdev.owner = THIS_MODULE;
		hello_dev[i].cdev.ops = &hello_op;
		hello_dev[i].size = DEVSIZE;
		if(err)	
			printk(KERN_NOTICE "Error %d adding hello%d", err, i);
	
	}
	return 0;
}

static void __exit hello_cleanup(void){
	kfree(hello_dev);
}

module_init(hello_init);
module_exit(hello_cleanup);

四 运行驱动,达成目标

​ 到现在,最主要的部分已经完成了,剩下比较繁琐地方就是要把我们的驱动让buildroot编译到它生成的文件系统中去。

4.1 buildroot安装APP

​ 其实这部分内容buildroot官方手册上有,不过说得有点太详细了,当时我看很多选项都是一知半解,也不敢乱删,所以这里介绍的是一个最简单的版本。

​ 首先在package/Config.in中为我们的hello创建一个菜单项,以便在make menuconfig中选择(所以的菜单项都在这个Config.in中管理)。

​ 推荐在Hardware Handling下输入:

menu “Hello”
source “package/hello/Config.in”
endmenu

这里那个source的意思是去指定的目录下寻找安装时的具体操作文件,这个文件惯例是package/App名称/Config.in,当然,你也可以选择在其他地方创建,只要能管理好目录的话。

​ 接下里在package目录下创建hello目录,里面创建两个文件,一个是Config.in,另一个是hello.mk。

​ Config.in的内容如下,这里说明了hello的依赖。

config BR2_PACKAGE_HELLO
bool “hello”
depends on BR2_LINUX_KERNEL
depends on BR2_PACKAGE_GLIBC
help
hello

​ hello.mk:

#################################################################################
#

#hello

#
################################################################################
HELLO_VERSION = 1.0
HELLO_SITE_METHOD = local
HELLO_SITE = $(TOPDIR)/dl/hello
HELLO_LICENSE = GPL-2.0
HELLO_LICENSE_FILES = LICENSE

define HELLO_BUILD_CMDS
( M A K E ) C C = " (MAKE) CC=" (MAKE)CC="(TARGET_CC)" LD=“$(TARGET_LD)” -C $(@D)
endef

define HELLO_INSTALL_TARGET_CMDS
$(INSTALL) -D -m 0755 $(@D)/hello.ko $(TARGET_DIR)/usr/bin
$(INSTALL) -D -m 0775 $(@D)/hello_user.o $(TARGET_DIR)/usr/bin
endef

$(eval $(kernel-module))
$(eval $(generic-package))

​ 最上面的#形成的区域不可以删去,这是固定格式。HELLO_SITE指定了要安装的APP所在的目录,也就是hello.ko,hello_user.o所在的目录,源码和Makefile也在这个目录下。

​ HELLO_BUILD_CMDS指定了编译时buildroot要如何去编译。

​ HELLO_INSTALL_TARGET_CMDS指定了如何安装APP,调用的是install命令,相关的参数也和该命令一致。

​ 最后两句是在调用buildroot的编译系统来进行一些必要的工作。

​ 最后一步就是来到dl目录下创建hello目录,把hello.c,hello_user.c,Makefile移到这个文件夹下。

​ 到buildroot目录下执行make menuconfig,这时可以在Hardware Handling中看到Hello菜单了,把hello模块勾选上,然后make一下,就能把hello.ko和hello.o安装到/buildroot生成的文件系统的usr/bin下,可以使用qmenu去查看一下。

4.2 运行程序

​ 接下来要完成2.1节中的三四五步。输入insmod hello.ko,到/proc/devices中查看HelloModule的MAJOR,如下图:

然后到/dev下输入:

mknod hello_dev c MAJOR 0

这里要注意设备名要与2.1节中hello_user.c中打开的文件相同,否则编译出来的hello_user是找不到相应的设备文件的。

​ 运行/usr/bin/hello_user.o,结果如下:

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值