Linux内核大讲堂 () 传说中的字符设备(3)

       接下来讲的是字符设备的重点,就是从用户空间调用open最到终调用我们写的字符驱动中的open的整个过程。下面的讨论将会非常有意思,请大家一定要看完所有的描述,否则理解有问题就别怪哥没提醒你了!

       在这之前我们大概说说大名鼎鼎的系统调用,从字面上理解就是系统(内核)提供的调用(服务),取小括号里面的话就拼成了:内核提供的服务。这个服务是通过一条int 0x80指令来实现的,利用这条指令可以产生一个软件中断,所谓的软件中断就是执行软件指令产生的一个假中断,这个假中断是相对于硬中断而言的。通过产生的假中断,我们可以做一个用户态到内核态的切换。就拥有了内核的执行权限。懂点底层的玩家都知道有什么ring0,ring1,ring2之类的, Arm玩家应该也知道ARM9体系的CPU7种工作模式。Linux内核就是利用了CPU的这种特性来实现内核态和用户态的划分。你要用CPU的特性,那当然要符合CPU定制的游戏规则罗。int这条指令所对应的机器码就是CPU制定的游戏规则。这个游戏规则就可以实现CPU工作模式的切换,在linux下就可以实现用户态到内核态的切换,而我们一些伟大的先人发现机器码不好记,易出错,所以就做了一个符号与这机器码一一对应,这些符号就就是今天大家所熟知的汇编指令。当年要换了我们去玩计算机的话,要一天到晚敲0101,我想大家也肯定不会愿意,也肯定会想些助记符来替换0101。如果换了是我的话,我肯定会把中断指令所对应机器码的指令名写成:wwhsint。让wwhs受后人敬仰,想不认识哥都难。^_^,但我们伟大的先人境界很高,为了让大家少敲4个字母就直接取名int(难道这就是传说中的无厘头解释?)。好了,用户态到内核态的切换已经完成了。

我们上面的推导是正确的吗?

我要告诉大家,不见得!我自已都差点被骗了。经过哥反汇编后,发现我本机的open系统调用并不是所谓的通过int 0x80来实现的。那到底是怎么实现的呢?你想知道吗?哥满足你强烈的求知欲。接下来这一段旅程将会非常有意思!请打起精神来。

下面是哥本机open函数的反汇编结果:

(gdb) disassemble open

Dump of assembler code for function open:

0x0072b1d0 <open+0>:    cmpl   $0x0,%gs:0xc

0x0072b1d8 <open+8>:    jne    0x72b1fc <open+44>

0x0072b1da <__open_nocancel+0>: push   %ebx

0x0072b1db <__open_nocancel+1>: mov    0x10(%esp),%edx

0x0072b1df <__open_nocancel+5>: mov    0xc(%esp),%ecx

0x0072b1e3 <__open_nocancel+9>: mov    0x8(%esp),%ebx

0x0072b1e7 <__open_nocancel+13>:        mov    $0x5,%eax

0x0072b1ec <__open_nocancel+18>:        call   *%gs:0x10

0x0072b1f3 <__open_nocancel+25>:        pop    %ebx

0x0072b1f4 <__open_nocancel+26>:        cmp    $0xfffff001,%eax

0x0072b1f9 <__open_nocancel+31>:        jae    0x72b22d <open+93>

0x0072b1fb <__open_nocancel+33>:        ret   

0x0072b1fc <open+44>:   call   0x747720 <__libc_enable_asynccancel>

0x0072b201 <open+49>:   push   %eax

0x0072b202 <open+50>:   push   %ebx

0x0072b203 <open+51>:   mov    0x14(%esp),%edx

0x0072b207 <open+55>:   mov    0x10(%esp),%ecx

0x0072b20b <open+59>:   mov    0xc(%esp),%ebx

0x0072b20f <open+63>:   mov    $0x5,%eax

0x0072b214 <open+68>:   call   *%gs:0x10

0x0072b21b <open+75>:   pop    %ebx

0x0072b21c <open+76>:   xchg   %eax,(%esp)

0x0072b21f <open+79>:   call   0x7476e0 <__libc_disable_asynccancel>

0x0072b224 <open+84>:   pop    %eax

0x0072b225 <open+85>:   cmp    $0xfffff001,%eax

0x0072b22a <open+90>:   jae    0x72b22d <open+93>

0x0072b22c <open+92>:   ret   

0x0072b22d <open+93>:   call   0x7742e8 <__i686.get_pc_thunk.cx>

0x0072b232 <open+98>:   add    $0x7edc2,%ecx

0x0072b238 <open+104>:  mov    -0x20(%ecx),%ecx

0x0072b23e <open+110>:  xor    %edx,%edx

0x0072b240 <open+112>:  sub    %eax,%edx

0x0072b242 <open+114>:  mov    %edx,%gs:(%ecx)

0x0072b245 <open+117>:  or     $0xffffffff,%eax

0x0072b248 <open+120>:  jmp    0x72b22c <open+92>

End of assembler dump.

 

通篇看不到传说中的int 0x80!很明显调openglibc中函数,那就调一下吧。我调啊调,调啊调,调了一会后,终于看到了有价值的东西:__kernel_vsyscall。继续反汇编__kernel_vsyscall

(gdb)  disassemble __kernel_vsyscall

Dump of assembler code for function __kernel_vsyscall:

0xb77dd414 <__kernel_vsyscall+0>:       push   %ecx

0xb77dd415 <__kernel_vsyscall+1>:       push   %edx

0xb77dd416 <__kernel_vsyscall+2>:       push   %ebp

0xb77dd417 <__kernel_vsyscall+3>:       mov    %esp,%ebp

0xb77dd419 <__kernel_vsyscall+5>:       sysenter

0xb77dd41b <__kernel_vsyscall+7>:       nop   

0xb77dd41c <__kernel_vsyscall+8>:       nop   

0xb77dd41d <__kernel_vsyscall+9>:       nop   

0xb77dd41e <__kernel_vsyscall+10>:      nop   

0xb77dd41f <__kernel_vsyscall+11>:      nop   

0xb77dd420 <__kernel_vsyscall+12>:      nop   

0xb77dd421 <__kernel_vsyscall+13>:      nop   

0xb77dd422 <__kernel_vsyscall+14>:      jmp    0xb77dd417 <__kernel_vsyscall+3>

0xb77dd424 <__kernel_vsyscall+16>:      pop    %ebp

0xb77dd425 <__kernel_vsyscall+17>:      pop    %edx

0xb77dd426 <__kernel_vsyscall+18>:      pop    %ecx

0xb77dd427 <__kernel_vsyscall+19>:      ret   

End of assembler dump.

咦!?sysenter???

有点像,汇编哥也不熟,只好google啦!经过google以后,这是我们的linus大神整出来的。这东西是对传统系统调用的优化。下面这是从IBM网站上找到的原话:sysenter 指令用于由 Ring3 进入 Ring0SYSEXIT 指令用于由 Ring0 返回 Ring3。由于没有特权级别检查的处理,也没有压栈的操作,所以执行速度比 INT n/IRET 快了不少!(但哥发现exit是使用int 0x80的,还有别的系统调用是否用int 0x80我没试,这个也不重要了)调用sysenter指令最终会跳转到汇编代码sysenter_do_call这个section中。(中间的细节处理就别抠了)

sysenter_do_call:

       cmpl $(nr_syscalls), %eax

       jae syscall_badsys

       call *sys_call_table(,%eax,4)

       movl %eax,PT_EAX(%esp)

       LOCKDEP_SYS_EXIT

       DISABLE_INTERRUPTS(CLBR_ANY)

       TRACE_IRQS_OFF

       movl TI_flags(%ebp), %ecx

       testl $_TIF_ALLWORK_MASK, %ecx

       jne sysexit_audit

终于又找到老朋友了!看:call *sys_call_table(,%eax,4)

sys_call_table是啥东西不知道?不会吧,哥们你out了!

ENTRY(sys_call_table)

       .long sys_restart_syscall /* 0 - old "setup()" system call, used for restarting */

       .long sys_exit

       .long ptregs_fork

       .long sys_read

       .long sys_write

       .long sys_open              /* 5 */

明白了吗?还不明白?.long是四个字节,对应了call指令的后面那个4eax对应的就是偏移号,比如open的话就是5call sys_call_table + 5*4就跳转到sys_open了。他娘的,linux内核中的技巧真的是无处不在,换了是哥的设计的话,可能就是这样了:

定义一个数字与函数的条目,然后根据条目的数量N定义一张大表,调用的时候再来个哈希。并且哥肯定认为这效率已经很高了。与linux内核比起来,效率基本是一样的,时间复杂度都是O1),但是我这样处理的话相对于linux内核的处理就多用掉了N*4个字节。如果有N等于256的话。哥就直接多浪费了1K!功力就是在细微处体现的,各位千万不要以为自已天下第一,在公司开发的项目当中如果你看了别人写的代码很别扭的话,千万不要在心里问候人家的祖宗,说不定人家水平很高,可能是你自已水平不够不会欣赏而已。哥当年工作一半年左右的时候是很高调的,感觉敲代码不过如此,现在回头想想,真是幼稚的可以了!

好了,接下来我们看年sys_open

sys_open在哪呢?linux的潜规则又来了,这个就靠摸索了,我非常不建议大家一有问题就去翻书,上网找资料。你要去源码里面找,或者自已想办法调试。不过这里哥已经帮你找好了。

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)

{

       long ret;

 

       if (force_o_largefile())

              flags |= O_LARGEFILE;

 

       ret = do_sys_open(AT_FDCWD, filename, flags, mode);

       /* avoid REGPARM breakage on x86: */

       asmlinkage_protect(3, ret, filename, flags, mode);

       return ret;

}

不明白?帖几个宏给你看一下就明白了。

第一个宏:

#define SYSCALL_DEFINE3(name, ...) SYSCALL_DEFINEx(3, _##name, __VA_ARGS__)

第二个宏:

#define SYSCALL_DEFINEx(x, sname, ...)                            /

       __SYSCALL_DEFINEx(x, sname, __VA_ARGS__)

第三个宏:

#define __SYSCALL_DEFINEx(x, name, ...)                                 /

       asmlinkage long sys##name(__SC_DECL##x(__VA_ARGS__))

其实就是用一个##sys与我们传入的open 连起来了。

一定要在学习内核的过程中不断提升自已的分析调试能力,这才是我们学内核的目的,不要为了学内核而学内核!

       我们先回到函数:

SYSCALL_DEFINE3(open, const char __user *, filename, int, flags, int, mode)

{

       long ret;

 

       if (force_o_largefile())

              flags |= O_LARGEFILE;

 

       ret = do_sys_open(AT_FDCWD, filename, flags, mode);

       /* avoid REGPARM breakage on x86: */

       asmlinkage_protect(3, ret, filename, flags, mode);

       return ret;

}

显然最重要的是do_sys_open(AT_FDCWD, filename, flags, mode);

long do_sys_open(int dfd, const char __user *filename, int flags, int mode)

{

       char *tmp = getname(filename);//获取文件名,因为传进来的字符串位于用户空间

       int fd = PTR_ERR(tmp);

 

       if (!IS_ERR(tmp)) {

              fd = get_unused_fd_flags(flags);

              if (fd >= 0) {

                     struct file *f = do_filp_open(dfd, tmp, flags, mode, 0);

                     if (IS_ERR(f)) {

                            put_unused_fd(fd);

                            fd = PTR_ERR(f);

                     } else {

                            fsnotify_open(f);

                            fd_install(fd, f);

                     }

              }

              putname(tmp);

       }

       return fd;

}

麻烦的事情终于来了。我们避不开了。

先来看看fd,看看大家天天用的fd到底是个啥玩意。

#define get_unused_fd_flags(flags) alloc_fd(0, (flags))

再看看alloc_fd:

int alloc_fd(unsigned start, unsigned flags)

{

       struct files_struct *files = current->files;

       unsigned int fd;

       int error;

       struct fdtable *fdt;

 

       spin_lock(&files->file_lock);

repeat:

       fdt = files_fdtable(files);

       fd = start;

       if (fd < files->next_fd)

              fd = files->next_fd;

 

       if (fd < fdt->max_fds)

              fd = find_next_zero_bit(fdt->open_fds->fds_bits,

                                      fdt->max_fds, fd);

 

       error = expand_files(files, fd);

       if (error < 0)

              goto out;

 

       /*

        * If we needed to expand the fs array we

        * might have blocked - try again.

        */

       if (error)

              goto repeat;

 

       if (start <= files->next_fd)

              files->next_fd = fd + 1;

 

       FD_SET(fd, fdt->open_fds);

       if (flags & O_CLOEXEC)

              FD_SET(fd, fdt->close_on_exec);

       else

              FD_CLR(fd, fdt->close_on_exec);

       error = fd;

#if 1

       /* Sanity check */

       if (rcu_dereference_raw(fdt->fd[fd]) != NULL) {

              printk(KERN_WARNING "alloc_fd: slot %d not NULL!/n", fd);

              rcu_assign_pointer(fdt->fd[fd], NULL);

       }

#endif

 

out:

       spin_unlock(&files->file_lock);

       return error;

}

首先解释一下struct files_struct *files = current->files;

current是一个宏:

#define current get_current()

利用这个宏我们可以取得当前进程的任务结构体,任务结构体中有一个成员:

struct files_struct *files;这个东东包含了与文件相关的一大堆东东,首先里面有个成员叫next_fd,每当open一个文件经就自加1。然后还定义了一个叫open_fds的位图,每个位表示对应数值的文件,比如第1位,表示fd1的文件,第100位,表示fd100的文件。这也是一种比较优秀的设计方法,, <<编译珠玑>>这本书讲到大量不重复的数进行排序时也就利用了这种思想。另外精通网络编程的同志肯定知道名震江湖的select了,select也用到了位图的思想。算法不是本系列的重点,如果不太明白的同志可以查阅相关文档进行学习。

好了,言归正传(我发现我老是容易跑题^_^),然后在调用alloc_fd()也就是get_unused_fd_flags()时,他就会帮我们分配一个fd,并且在位图中帮我们打上已经占用的标记。真正实现打标记功能的其实就是__FD_SET

#define __FD_SET(fd,fdsetp)                                   /

       asm volatile("btsl %1,%0":                        /

                   "+m" (*(__kernel_fd_set *)(fdsetp))   /

                   : "r" ((int)(fd)))

不懂汇编的同志不用急,这个是GCC提供的内嵌汇编,也不难,可以参照相关的文档花一两个小时学习一下就差不多了。这里我说一下我的学习方法:汇编的指令我是从来不记的,用到了就查一下,我人本来就笨,记性又不好,所以能不记的东西我是从来不记的。

接下来就是do_filp_open(dfd, tmp, flags, mode, 0);

接下来我直接给出调用顺序,(文件系统后面会有专题进行分析)

do_filp_open()-> finish_open()-> nameidata_to_filp()-> __dentry_open()

接下来我们看看__dentry_open():

static struct file *__dentry_open(struct dentry *dentry, struct vfsmount *mnt,

                                   struct file *f,

                                   int (*open)(struct inode *, struct file *),

                                   const struct cred *cred)

{

       struct inode *inode;

       int error;

 

       f->f_mode = OPEN_FMODE(f->f_flags) | FMODE_LSEEK |

                            FMODE_PREAD | FMODE_PWRITE;

       inode = dentry->d_inode;

       if (f->f_mode & FMODE_WRITE) {

              error = __get_file_write_access(inode, mnt);

              if (error)

                     goto cleanup_file;

              if (!special_file(inode->i_mode))

                     file_take_write(f);

       }

 

       f->f_mapping = inode->i_mapping;

       f->f_path.dentry = dentry;

       f->f_path.mnt = mnt;

       f->f_pos = 0;

       f->f_op = fops_get(inode->i_fop);

       file_sb_list_add(f, inode->i_sb);

 

       error = security_dentry_open(f, cred);

       if (error)

              goto cleanup_all;

 

       if (!open && f->f_op)

              open = f->f_op->open;

       if (open) {

              error = open(inode, f);

              if (error)

                     goto cleanup_all;

       }

       ima_counts_get(f);

 

       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) &&

                  (!f->f_mapping->a_ops->get_xip_mem))) {

                     fput(f);

                     f = ERR_PTR(-EINVAL);

              }

       }

 

       return f;

 

cleanup_all:

       fops_put(f->f_op);

       if (f->f_mode & FMODE_WRITE) {

              put_write_access(inode);

              if (!special_file(inode->i_mode)) {

                     /*

                      * We don't consider this a real

                      * mnt_want/drop_write() pair

                      * because it all happenend right

                      * here, so just reset the state.

                      */

                     file_reset_write(f);

                     mnt_drop_write(mnt);

              }

       }

       file_sb_list_del(f);

       f->f_path.dentry = NULL;

       f->f_path.mnt = NULL;

cleanup_file:

       put_filp(f);

       dput(dentry);

       mntput(mnt);

       return ERR_PTR(error);

}

看到了吗?

if (!open && f->f_op)

              open = f->f_op->open; //这就是我们之前注册的wwhs_open()

       if (open) {

              error = open(inode, f);

              if (error)

                     goto cleanup_all;

 

好了,虽然有很多细节我们没有详细分析,但是open函数从用户空间到我们最终的wwhs_open的整个调用的脉络大家都非常清楚了。read()write()等函数的调用过程比这个就简单多了。

调用顺序大致如下:

sysenter_do_call()->sys_read()->vfs_read()->wwhs_read()

字符设备的讲解到这里要告一断落了,是不是感觉打通了一条经脉但另一条经脉又阻塞了?呵呵,该来的总归要来,出来混迟早要还的,我们无论是在讲驱动模型,还是讲字符驱动都避开文件系统没讲,现在调用的脉络及原理大家都清楚了,但细节上的把握还差的很远,要把握住细节,就避不开文件系统,所以下一节就会讲文件系统了!^_^

 

GitHub 加速计划 / li / linux-dash
10.39 K
1.2 K
下载
A beautiful web dashboard for Linux
最近提交(Master分支:2 个月前 )
186a802e added ecosystem file for PM2 4 年前
5def40a3 Add host customization support for the NodeJS version 4 年前
Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐