#ifdef __USE_ATFILE
/* Remove the link NAME relative to FD. */
extern int unlinkat (int __fd, const char *__name, int __flag)
__THROW __nonnull ((2));
#endif
/* Remove the directory PATH. */
extern int rmdir (const char *__path) __THROW __nonnull ((1));
/* The *at syscalls were introduced just after 2.6.16-rc1. Due to the way the
kernel versions are advertised we can only rely on 2.6.17 to have
the code. On PPC they were introduced in 2.6.17-rc1,
on SH in 2.6.19-rc1. */
#if __LINUX_KERNEL_VERSION >= 0x020611 \
&& (!defined __sh__ || __LINUX_KERNEL_VERSION >= 0x020613)
# define __ASSUME_ATFCTS 1
#endif
从"Use CPUID to check if SYSCALL and SYSRET are available (CPUID.80000001H.EDX[bit 11] = 1)"这一句可以看出,在调用前需要置edx寄存器中的11位来使能64位平台的syscall/sysret,好的我们找出edx寄存器相关。
[qianzichen@dev03v /src/linux/linux]$ vi arch/x86/entry/syscalls/syscall_64.tbl
这是一个列表文件,
#
# 64-bit system call numbers and entry vectors
#
# The format is:
# <number> <abi> <name> <entry point>
#
# The abi is "common", "64" or "x32" for this file.
#
0 common read sys_read
...
261 common futimesat sys_futimesat
262 common newfstatat sys_newfstatat
263 common unlinkat sys_unlinkat
264 common renameat sys_renameat
265 common linkat sys_linkat
...
#
# x32-specific system call numbers start at 512 to avoid cache impact
# for native 64-bit operation.
#
512 x32 rt_sigaction compat_sys_rt_sigaction
...
common代表32/64位平台通用
user space 和 kernel space 的 system call 映射建立。
其实kernel space对编号的映射不是这么简单,这里不再展开。
我们大概知道 user space 的 unlinkat 最终在 kernel space 的 entry point 是 sys_unlinkat 就好了。
还是直接查看汇编代码吧:
[qianzichen@dev03v /src/linux/linux]$ vi arch/x86/entry/entry_64.S
...
ENTRY(entry_SYSCALL_64)
/*
* Interrupts are off on entry.
* We do not frame this tiny irq-off block with TRACE_IRQS_OFF/ON,
* it is too small to ever cause noticeable irq latency.
*/
SWAPGS_UNSAFE_STACK
movq %rsp, PER_CPU_VAR(rsp_scratch)
movq PER_CPU_VAR(cpu_current_top_of_stack), %rsp
TRACE_IRQS_OFF
/* Construct struct pt_regs on stack */
pushq $__USER_DS
...
ja 1f /* return -ENOSYS (already in pt_regs->ax) */
movq %r10, %rcx
/*
* This call instruction is handled specially in stub_ptregs_64.
* It might end up jumping to the slow path. If it jumps, RAX
* and all argument registers are clobbered.
*/
call *sys_call_table(, %rax, 8)
...
END(entry_SYSCALL_64)
3941 /**
3942 * vfs_unlink - unlink a filesystem object
3943 * @dir: parent directory
3944 * @dentry: victim
3945 * @delegated_inode: returns victim inode, if the inode is delegated.
3946 *
3947 * The caller must hold dir->i_mutex.
3948 *
3949 * If vfs_unlink discovers a delegation, it will return -EWOULDBLOCK and
3950 * return a reference to the inode in delegated_inode. The caller
3951 * should then break the delegation on that inode and retry. Because
3952 * breaking a delegation may take a long time, the caller should drop
3953 * dir->i_mutex before doing so.
3954 *
3955 * Alternatively, a caller may pass NULL for delegated_inode. This may
3956 * be appropriate for callers that expect the underlying filesystem not
3957 * to be NFS exported.
3958 */
3959 int vfs_unlink(struct inode *dir, struct dentry *dentry, struct inode **delegated_inode)
3960 {
3961 struct inode *target = dentry->d_inode;
3962 int error = may_delete(dir, dentry, 0);
3963
3964 if (error)
3965 return error;
3966
3967 if (!dir->i_op->unlink)
3968 return -EPERM;
3969
3970 inode_lock(target);
3971 if (is_local_mountpoint(dentry))
3972 error = -EBUSY;
3973 else {
3974 error = security_inode_unlink(dir, dentry);
3975 if (!error) {
3976 error = try_break_deleg(target, delegated_inode);
3977 if (error)
3978 goto out;
3979 error = dir->i_op->unlink(dir, dentry);
3980 if (!error) {
3981 dont_mount(dentry);
3982 detach_mounts(dentry);
3983 }
}
3985 }
3986 out:
3987 inode_unlock(target);
3988
3989 /* We don't d_delete() NFS sillyrenamed files--they still exist. */
3990 if (!error && !(dentry->d_flags & DCACHE_NFSFS_RENAMED)) {
3991 fsnotify_link_count(target);
3992 d_delete(dentry);
3993 }
3994
3995 return error;
3996 }
3997 EXPORT_SYMBOL(vfs_unlink);
后端有时候rm,会出现一些问题。
这里作为一个子问题,讨论一下rm之后,发生的一些事。
打开rm源码:
从main函数开始:
首先解析命令行参数,然后调用了rm:
作者把rm函数的实现从rm.c中抽了出来,放在remove.c中:
file参数是一个只读指针数组,代表要删除的文件名列表,x参数的结构定义如下,存储从命令行中解析后的rm的选项。
当file列表存在时,rm调用xfts_open:
xfts_open返回fts_open的有效返回值。fts_open的实现如下:
引用中已去除了一些Error handling,可以看出主要是获取文件系统的一些信息,保存在FTS结构中,FTS结构定义如下:
再回到rm函数,它将在一个loop中通过fts_read读取文件系统信息,并缓存在ent中:
ent的结构比较大,这里不展开了。
再通过rm_fts对某一个ent进行操作,这里我们rm的是一个regular file,所以控制结构会执行到FTS_F分支下,最终调用execise。
这里再次忽略一些容错和优化,execise最终调用了unlinkat
如上我们看出,rm最终调用了unlinkat这一核心函数,比如,删除a.txt:
用户态rm调用了C库中的unlinkat,经查找,其声明是在中
用户态进程只要调用unlink函数就可以了,具体unlinkat函数的实现是由glibc 提供的,其定义在io/unlink.c中:
额好吧,这儿是个弱符号,真正的实现在./sysdeps/unix/sysv/linux/unlinkat.c
syscall的name为_NR##name,通过宏中字符串粘合而得本例中的__NR_unlinkat。其定义在/usr/include/asm/unistd_64.h中。
所以该宏被启用。
显然可以看出,若kernel版本在2.6.17之后,__ASSUME_ATFCTS宏被启用。无需校验__have_atfcts >= 0,直接调用INLINE_SYSCALL (unlinkat, 3, fd, file, flag)。
这里直接看底层实现吧(./sysdeps/unix/sysv/linux/x86_64/sysdep.h),是一段内联汇编:
在syscall之前先将参数传入寄存器。返回值在eax寄存器中,通常0表示成功。
从C库代码上来看,就是这么实现了的,rm实用程序调用glibc,然后再到汇编syscall -> kernel
但是当前机器安装的不一定是upstream的C库。
我们还是来亲眼看一下最终机器码是如何实现的吧,我这里直接反汇编一下:
这里可以看到glibc-2.17最终使用了一些AT&T syntax Assembly language。
先用一个比较新的指令movslq,把第一个寄存器扩展到64位并复制到第二个寄存器中,不填充符号位。
下一步,将0x107这个值载入eax寄存器
随后,调用syscall指令。
打开Intel的相关芯片手册,搜索“syscall”,找到相关描述如下图。 从这段描述中看出,syscall是Intel对64位处理器做的优化,被设计用来为操作系统提供一个平面内存模式,我的当前64位机器,syscall/sysret就和32位体系上的sysenter/sysexit的作用相似,可能和旧平台的int 80中断类似,主要是将CPU运行级别从level 3升级为level 0,操作一些应用层无法访问的资源。
从"Use CPUID to check if SYSCALL and SYSRET are available (CPUID.80000001H.EDX[bit 11] = 1)"这一句可以看出,在调用前需要置edx寄存器中的11位来使能64位平台的syscall/sysret,好的我们找出edx寄存器相关。
之前操作edx寄存器,就是“使能bit 11位和bit 29”这种准备工作。
我们确定了,unlinkat是一个system call,rm实用程序将删除文件的任务交给操作系统,至此程序陷入内核态。
好的,我们现在到kernel下,直接搜索unlinkat:
直接看x86体系下的源码:
这是一个列表文件,
这里看出,unlinkat对应的number是263 还记得写入eax寄存器中的值吗,是0x107。 很显然,0x107 = 1 16 ^ 2 + 0 16 ^ 1 + 7 * 16 ^ 0 = 263
common代表32/64位平台通用 user space 和 kernel space 的 system call 映射建立。
其实kernel space对编号的映射不是这么简单,这里不再展开。
我们大概知道 user space 的 unlinkat 最终在 kernel space 的 entry point 是 sys_unlinkat 就好了。
还是直接查看汇编代码吧:
rax中存的就是这次syscall的num,即__NR_unlinkat。
ENTRY(entry_SYSCALL_64)是64位的 syscall 汇编入口点,在准备一系列寄存器之后,call *sys_call_table(, %rax, 8)将跳转到系统调用表中的偏移地址,也就是sys_call_table数组中下标为syscall num对应的函数。
sys_call_table在另一个文件中定义,这里用到了一点编译器扩展和预编译技术的一种高效用法,这里也不再展开。
什么时候建立syscall number和sys_unlinkat的映射呢?这要看<asm/syscalls_64.h>,这个头文件是一个过程文件,在编译时生成。原映射信息就是从上文提到的./arch/x86/entry/syscalls/syscall_64.tbl中获得。
编译出来的syscalls_64.h结果为:
SYSCALL_COMMON就是__SYSCALL_64,如上文述sys_call_table的定义,第一个SYSCALL_64的定义是为了将syscalls_64.h展开为函数声明,之后将__SYSCALL_64重新定义后,是为了将syscalls_64.h展开为数组成员的定义。
所以最终内核得到的,是一个只读的sys_call_table数组,下标为syscall number,指向的是内核的sys_call_ptr_t。syscall num从0开始,所以直接根据263就可以找到sys_unlinkat。
现在内核已经确定了要调用的是sys_unlinkat,那么这个函数在哪里定义的呢?经过我的一番尝试,4.9中直接找sys_unlinkat是找不到实现的,因为这个字符串可能经过预编译粘合。
我最终找到的宏是这样定义的:
然后找到,sys_unlinkat的代码在fs/namei.c中:
然后调用do_unlinkat:
好了,读者随着我到这一步,已经看到了软件工程中比较具有美感的一个地方:4044行,调用了vfs_unlink。从user space到system call再至此,sys_unlinkat将unlinkat的任务,dispatch给操作系统的虚拟文件系统。
我们看一下vfs_unlink的实现:
我们看到,3979行,调用inode实例中i_op成员的unlink函数指针,这个指针才指向了真正的HAL层实现。
现在看inode结构的定义:
可以看到上文的inode实例中的i_op成员是一个inode_operations结构指针。
现在看inode_operations的定义:
vfs下层的各种文件系统,需要按照inode_operations中的规范,完成unlink的实现,向kernel vfs注册。
这里不展开bootloader自举之后的硬件初始化,也忽略kernel接管机器资源之后的一些register机制,直接看当前机器是怎么向vfs最终注册。
看了一下,我机器上挂载的是ext4文件系统,直接看ext4的unlink的最终注册过程:
ext4_dir_inode_operations实例中,完成了函数指针的赋值。
直接看ext4_unlink的实现:
看d_inode的实现:
d_inode(dentry)将inode信息从dentry结构中取出来,dentry结构定义如下:
dentry这一层,不是简单的从硬盘中移除。为了高性能,当前ext4对目录做了一些缓存处理。应该是先设置标志位,然后根据sync机制回写存储。
vfs之下的机制就先不详述了,因为我也不太清楚,蛤蛤。
Linkerist 2018年1月19日于北京酒仙桥