Linux内核大讲堂 (二) 传说中的字符设备(3)
Linux内核大讲堂 (二) 传说中的字符设备(3)
接下来讲的是字符设备的重点,就是从用户空间调用open最到终调用我们写的字符驱动中的open的整个过程。下面的讨论将会非常有意思,请大家一定要看完所有的描述,否则理解有问题就别怪哥没提醒你了!
在这之前我们大概说说大名鼎鼎的系统调用,从字面上理解就是系统(内核)提供的调用(服务),取小括号里面的话就拼成了:内核提供的服务。这个服务是通过一条int 0x80指令来实现的,利用这条指令可以产生一个软件中断,所谓的软件中断就是执行软件指令产生的一个假中断,这个假中断是相对于硬中断而言的。通过产生的假中断,我们可以做一个用户态到内核态的切换。就拥有了内核的执行权限。懂点底层的玩家都知道有什么ring0,ring1,ring2之类的, Arm玩家应该也知道ARM9体系的CPU有7种工作模式。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!很明显调open是glibc中函数,那就调一下吧。我调啊调,调啊调,调了一会后,终于看到了有价值的东西:__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 进入 Ring0,SYSEXIT 指令用于由 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指令的后面那个4,eax对应的就是偏移号,比如open的话就是5。call sys_call_table + 5*4就跳转到sys_open了。他娘的,linux内核中的技巧真的是无处不在,换了是哥的设计的话,可能就是这样了:
定义一个数字与函数的条目,然后根据条目的数量N定义一张大表,调用的时候再来个哈希。并且哥肯定认为这效率已经很高了。与linux内核比起来,效率基本是一样的,时间复杂度都是O(1),但是我这样处理的话相对于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位,表示fd为1的文件,第100位,表示fd为100的文件。这也是一种比较优秀的设计方法,, <<编译珠玑>>这本书讲到大量不重复的数进行排序时也就利用了这种思想。另外精通网络编程的同志肯定知道名震江湖的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()。
字符设备的讲解到这里要告一断落了,是不是感觉打通了一条经脉但另一条经脉又阻塞了?呵呵,该来的总归要来,出来混迟早要还的,我们无论是在讲驱动模型,还是讲字符驱动都避开文件系统没讲,现在调用的脉络及原理大家都清楚了,但细节上的把握还差的很远,要把握住细节,就避不开文件系统,所以下一节就会讲文件系统了!^_^
更多推荐
所有评论(0)