Linux Open系统调用篇一html
Linux Open系统调用篇二linux
内核源码:linux-4.4 目标平台:ARM体系结构 源码工具:source insight 4数组
说明: 文中因为 md 语法问题,没法在代码高亮的同时而忽略因为
__
或者*
形成斜体的 问题,因此相似__user
改为__ user
,或者char *filename
改为char* filename
。 经过在中间添加空格进行避免。注释统一使用了\\
。缓存
应用层的 open
函数是 glibc 库封装了系统调用以比较友好的方式提供给开发者。 那么为何要这么作? 这主要是从安全以及性能这两大方面进行了考虑:安全
在用户空间和内核空间之间,有一个叫作Syscall(系统调用, system call)的中间层,是链接用 户态和内核态的桥梁。这样即提升了内核的安全型,也便于移植, 只需实现同一套接口便可。Linux系统,用户空间经过向内核空间发出Syscall,产生软中断, 从而让程序陷入内核态,执行相应的操做。对于每一个系统调用都会有一个对应的系统调用号 ,比不少操做系统要少不少。markdown
安全性与稳定性:内核驻留在受保护的地址空间,用户空间程序没法直接执行内核代码 ,也没法访问内核数据,经过系统调用数据结构
性能:Linux上下文切换时间很短,以及系统调用处理过程很是精简,内核优化得好,因此性能上 每每比不少其余操做系统执行要好。app
在应用层对于 open
操做主要使用的是如下两个函数:
(1) int open(const char *pathname, int flags, mode_t mode); (2) int openat(int dirfd, const char *pathname, int flags, mode_t mode); 复制代码
若是打开文件成功,那么返回文件描述符,值大于或等于0;若是打开文件失败,返 回负的错误号。
下面是该函数参数的说明:
openat
中解释为相对文件描述符 dirfd
引用的目录,open
函数中解释为相对 调用进程的当前工做目录。若是文件路径是绝对路径, openat
忽略参数 dirfd
。O_RDONLY
(只读)、O_ WRONLY
(只写)或 O_RDWR
(读写)。参数 flags
能够包含多个文件建立标志和文件状态标志。 两组标志的区别是: 文件建立标志只影响打开操做, 文件状态标志影响后面的读写操做。文件建立标志包括以下:
O_CLOEXEC
:开启 close-on-exc标志,使用系统调用 execve() 装载程序的时候关闭文件。CREAT
:若是文件不存在,建立文件。ODIRECTORY
:参数 pathname 必须是一个日录。EXCL
:一般和标志位 CREAT 联合使用,用来建立文件。若是文件已经存在,那么 open() 失败,返回错误号 EEXIST。NOFOLLOW
:不容许参数 pathname 是符号连接,即最后一个份量不能是符号 连接,其余份量能够是符号连接。若是参数 pathname 是符号连接,那么打开失败,返回错误号 ELOOP。O_TMPFILE
:建立没有名字的临时普通文件,参数 pathname 指定目录关闭文件的时候,自动删除文件。O_TRUNC
:若是文件已经存在,是普通文件而且访问模式容许写,那么把文件截断到长度为0。文件状态标志包括以下:
APPEND
:使用追加模式打开文件,每次调用 write 写文件的时候写到文件的末尾。O_ASYNC
:启用信号驱动的输入输出,当输入或输出可用的时候,发送信号通知进程,默认的信号是 SIGIO。O_DIRECT
:直接读写存储设备,不使用内核的页缓存。虽然会下降读写速度, 可是在某些状况下有用处,例如应用程序使用本身的缓冲区,不须要使用内核的页缓存文件。DSYNC
:调用 write 写文件时,把数据和检索数据所须要的元数据写回到存储设备LARGEFILE
:容许打开长度超过 4 GB 的大文件。NOATIME
:调用 read 读文件时,不要更新文件的访问时间。O_NONBLOCK
:使用非阻塞模式打开文件, open 和之后的操做不会致使调用进程阻塞。PATH
:得到文件描述符有两个用处,指示在目录树中的位置以及执行文件描述符层次的操做。 不会真正打开文件,不能执行读操做和写操做。O_SYNC
:调用 write 写文件时,把数据和相关的元数据写回到存储设备。参数 mode: 参数 mode 指定建立新文件时的文件模式。当参数 flags 指定标志位 O_CREAT
或 O_TMPFILE
的时候,必须指定参数 mode,其余状况下忽略参数 mode。 参数 mode 能够是下面这些标准的文件模式位的组合。
S_IRWXU
(0700,以0开头表示八进制):用户(即文件拥有者)有读、写和执行权限。S_IRUSR
(00400):用户有读权限。S_IWUSR
(00200):用户有写权限S_IXUSR
(00100):用户有执行权限。S_IRWXG
(00070):文件拥有者所在组的其余用户有读、写和执行权限S_IRGRP
(00040):文件拥有者所在组的其余用户有读权限。S_IWGRP
(00020):文件拥有者所在组的其余用户有写权限。S_IXGRP
(0010):文件拥有者所在组的其余用户有执行权限。S_IRWXO
(0007):其余组的用户有读、写和执行权限。S_IROTH
(0004):其余组的用户有读权限。S_IWOTH
(00002):其余组的用户有写权限。S_IXOTH
(00001):其余组的用户有执行权限。参数 mode 能够包含下面这些 Linux 私有的文件模式位:
S_ISUID
(0004000):set-user-ID 位。S_ISGID
(0002000):set-group-iD位。S_ISVTX
(0001000):粘滞(sticky)位。以上内容能够参考:open man7手册
那么咱们该如何找到对应的 syscall
? 有几个小技巧能够用来帮助咱们:
细节能够参考下面给出的连接:Linux系统调用(syscall)原理。
根据第一个小技巧,咱们知道咱们须要找的函数为:sys_open
。 具体代码流程比较复杂,这里使用取巧的方式,找到对应的内核函数,前面提到须要找的的函数 为 sys_open
。 这种函数在内核中是经过宏定义 SYSCALL_DEFINEx
展开后获得的。那么能够 利用 source insight
的搜索功能。应用层 open
函数的参数的个数为 3,能够假想先从 SYSCALL_DEFINE3
进行全局搜索。随便选择一个搜索结果,这里假设选择的是 SYSCALL_DEFINE3(mknod
,这步主要是为了获取代码格式,把 mknod
改为 open
,而后搜索 SYSCALL_DEFINE3(open
。 很快咱们就在 kernel\fs\open.c
文件中找到惟一的搜索结果,代码以下:
SYSCALL_DEFINE3(open, const char __ user*, filename, int, flags, umode_t, mode) { if (force_o_largefile()) flags |= O_LARGEFILE; return do_sys_open(AT_FDCWD, filename, flags, mode); } 复制代码
if (force_o_largefile()) flags |= O_LARGEFILE; 复制代码
表示 flags 会在 64 位 Kernel 的状况下强制么设置 O_LARGEFILE
来表示支持大文件。 接着跳转到 do_sys_open
函数。
long do_sys_open(int dfd, const char __ user *filename, int flags, umode_t mode) { struct open_flags op; //检查并包装传递进来的标志位 int fd = build_open_flags(flags, mode, &op); struct filename * tmp; if (fd) return fd; //用户空间的路径名复制到内核空间 tmp = getname(filename); if (IS_ERR(tmp)) return PTR_ERR(tmp); //获取一个未使用的 fd 文件描述符 fd = get_unused_fd_flags(flags); if (fd >= 0) { //调用 do_filp_open 完成对路径的搜寻和文件的打开 struct file * f = do_filp_open(dfd, tmp, &op); if (IS_ERR(f)) { //若是发生了错误,释放已分配的 fd 文件描述符 put_unused_fd(fd); //释放已分配的 struct file 数据 fd = PTR_ERR(f); } else { fsnotify_open(f); //绑定 fd 与 f。 fd_install(fd, f); } } //释放已分配的 filename 结构体。 putname(tmp); return fd; } 复制代码
fd 是一个整数,它实际上是一个数组的下标,用来获取指向 file 描述符的指针, 每一个进程都有个 task_struct 描述符用来描述进程相关的信息,其中有个 files_struct 类型的 files
字段,里面有个保存了当前进程全部已打开文件 描述符的数组,而经过 fd 就能够找到具体的文件描述符,之间的关系能够参考下图:
这里的参数已经在上面提到过了,惟一须要注意的是 AT_FDCWD
,其定义在 include/uapi/linux/fcntl.h
,是一个特殊值(** -100 **), 该值代表当 filename 为相对路径的状况下将当前进程的工做目录设置为起始路径。相对而言, 你能够在另外一个系统调用 openat 中为这个起始路径指定一个目录, 此时 AT_FDCWD
就会被该目录的描述符所替代。
static inline int build_open_flags(int flags, umode_t mode, struct open_flags *op) { int lookup_flags = 0; //O_CREAT 或者 `__O_TMPFILE*` 设置了,acc_mode 才有效。 int acc_mode; // Clear out all open flags we don't know about so that we don't report // them in fcntl(F_GETFD) or similar interfaces. // 只保留当前内核支持且已被设置的标志,防止用户空间乱设置不支持的标志。 flags &= VALID_OPEN_FLAGS; if (flags & (O_CREAT | __ O_TMPFILE)) op->mode = (mode & S_IALLUGO) | S_IFREG; else //若是 O_CREAT | __ O_TMPFILE 标志都没有设置,那么忽略 mode。 op->mode = 0; // Must never be set by userspace flags &= ~FMODE_NONOTIFY & ~O_CLOEXEC; // O_SYNC is implemented as __ O_SYNC|O_DSYNC. As many places only // check for O_DSYNC if the need any syncing at all we enforce it's // always set instead of having to deal with possibly weird behaviour // for malicious applications setting only __ O_SYNC. if (flags & __ O_SYNC) flags |= O_DSYNC; //若是是建立一个没有名字的临时文件,参数 pathname 用来表示一个目录, //会在该目录的文件系统中建立一个没有名字的 iNode。 if (flags & __ O_TMPFILE) { if ((flags & O_TMPFILE_MASK) != O_TMPFILE) return -EINVAL; acc_mode = MAY_OPEN | ACC_MODE(flags); if (!(acc_mode & MAY_WRITE)) return -EINVAL; } else if (flags & O_PATH) { // If we have O_PATH in the open flag. Then we // cannot have anything other than the below set of flags // 若是设置了 O_PATH 标志,那么 flags 只能设置如下 3 个标志。 flags &= O_DIRECTORY | O_NOFOLLOW | O_PATH; acc_mode = 0; } else { acc_mode = MAY_OPEN | ACC_MODE(flags); } op->open_flag = flags; // O_TRUNC implies we need access checks for write permissions // 若是设置了,那么写以前可能须要清空内容。 if (flags & O_TRUNC) acc_mode |= MAY_WRITE; // Allow the LSM permission hook to distinguish append // access from general write access. // 让 LSM 有能力区分 追加访问和普通访问。 if (flags & O_APPEND) acc_mode |= MAY_APPEND; op->acc_mode = acc_mode; //设置意图,若是没有设置 O_PATH,表示这次调用有打开文件的意图。 op->intent = flags & O_PATH ? 0 : LOOKUP_OPEN; if (flags & O_CREAT) { //是否有建立文件的意图 op->intent |= LOOKUP_CREATE; if (flags & O_EXCL) op->intent |= LOOKUP_EXCL; } //判断查找的目标是不是目录。 if (flags & O_DIRECTORY) lookup_flags |= LOOKUP_DIRECTORY; //判断当发现符号连接时是否继续跟下去 if (!(flags & O_NOFOLLOW)) lookup_flags |= LOOKUP_FOLLOW; //查找标志设置了 LOOKUP_FOLLOW 表示会继续跟下去。 //设置查找标志,lookup_flags 在路径查找时会用到 op->lookup_flags = lookup_flags; return 0; } 复制代码
上面的函数主要是根据用户传递进来的 flags 进一步设置具体的标志,而后把这些标志封装到 open_flags 结构体中。以便后续使用。
接下来就是函数 getname()
,这个函数定义在 fs/namei.c
,主体是 getname_flags
, 咱们捡重点的分析,可有可无的代码以 ... 略过。
struct filename * getname(const char __ user *filename) { return getname_flags(filename, 0, NULL); } 复制代码
struct filename { const char* name; // pointer to actual string ---指向真实的字符串 const __ user char* uptr; // original userland pointer -- 指向原来用户空间的 filename struct audit_names* aname; int refcnt; const char iname[]; //用来保存 pathname }; 复制代码
struct filename * getname_flags(const char __ user *filename, int flags, int* empty) { struct filename* result; char* kname; int len; // 这里通常来讲赋值为 NULL。这里主要是针对Linux 审计工具 audit,咱们无论。 result = audit_reusename(filename); // 若是不为空直接返回。 if (result) return result; // 经过__getname 在内核缓冲区专用队列里申请一块内存用来放置路径名(filemname 结构体) result = __getname(); if (unlikely(!result)) return ERR_PTR(-ENOMEM); //First, try to embed the struct filename inside the names_cache //allocation //kname 指向 struct filename 的 iname 数组。 kname = (char*)result->iname; // 把 filename->name 指向 iname[0],待会 iname 用来保存用户空间传递过来的路径名(filemname 结构体)。 result->name = kname; //该函数把用户空间的 filename 复制到 iname len = strncpy_from_user(kname, filename, EMBEDDED_NAME_MAX); //若是复制失败,释放已分配的 result 并返回错误。 if (unlikely(len < 0)) { __putname(result); return ERR_PTR(len); } // Uh-oh. We have a name that's approaching PATH_MAX. Allocate a // separate struct filename so we can dedicate the entire // names_cache allocation for the pathname, and re-do the copy from // userland. // 这里判断用户空间传递过来的路径名的长度接近了 PATH_MAX,因此须要分配一个独立的空间 // 用来保存 struct filename 前面的字段,并把 name_cache 所有空间用来保存路径名 (filename->iname)。 // // #define PATH_MAX 4096 // 4 kb 大小。 // #define offsetof(TYPE, MEMBER) ((size_t) &((TYPE * )0)->MEMBER) // #define EMBEDDED_NAME_MAX (PATH_MAX - offsetof(struct filename, iname)) // EMBEDDED_NAME_MAX 指的就是:字段 iname 在 filename 结构体中的偏移。 if (unlikely(len == EMBEDDED_NAME_MAX)) { // 注意,这里是把 iname[1] 的偏移赋值给了 size。这样 size 的大小包含了 inaem[0] // 能够用来保存 iname 数组的首地址。 const size_t size = offsetof(struct filename, iname[1]); // 把旧 result 的首地址赋值给了 kanme。 kname = (char * )result; // size is chosen that way we to guarantee that // result->iname[0] is within the same object and that // kname can't be equal to result->iname, no matter what. // 分配一个独立空间用来保存 filename,这样就能够把 filename 分离出来。 result = kzalloc(size, GFP_KERNEL); //分配失败,释放资源并返回错误。 if (unlikely(!result)) { __putname(kname); return ERR_PTR(-ENOMEM); } // 把原来的 filename 的首地址赋值给新分配的 result。这样就实现了分离。 result->name = kname; // 把用户空间的 filename 复制到 kname(name_cache 起始地址)。 len = strncpy_from_user(kname, filename, PATH_MAX); // 原来: // filename struct(内核空间,用 name_cach 来保存) // result ---> name_cache-----> name // uptr // aname // .... 复制操做(strncpy_from_user()) // iname <--------------> filename struct(用户空间) // // 如今: // filename struct(内核空间,注意这里是新开独立的空间。) // result ---> name ------> name_cache <---------------> filename struct(用户空间) // uptr 复制操做(strncpy_from_user()) // aname // .... // iname // 新分配的 filename 的首地址指向 name_cach,而 name_cach 又保存了用户 // 空间的 filename,因此新的 filename(result) 能间接访问到用户空间的 filename。 // 复制失败,释放资源,返回。 if (unlikely(len < 0)) { __putname(kname); kfree(result); return ERR_PTR(len); } // 路径过长,一样返回错误(从这里也能够看出,在 Linux 中路径名的长度不能超过 4096 字节)。 if (unlikely(len == PATH_MAX)) { __putname(kname); kfree(result); return ERR_PTR(-ENAMETOOLONG); } } // 引用计数为 1 result->refcnt = 1; // The empty path is special.空路径的处理。 if (unlikely(!len)) { if (empty) * empty = 1; // 若是 LOOKUP_EMPTY 没有设置,也就是本次 open 操做的目标不是空路径,可是传递了一个 // 空路径,因此返回错误。 if (! (flags & LOOKUP_EMPTY)) { //回收资源 putname(result); return ERR_PTR(-ENOENT); } } // 指向用户空间的 filename result->uptr = filename; result->aname = NULL; audit_getname(result); return result; } 复制代码
struct filename { const char* name; //pointer to actual string ---指向真实的字符串 const __ user char* uptr; //original userland pointer --- 指向原来用户空间 struct audit_names* aname; int refcnt; const char iname[]; //用来保存 pathname }; 复制代码
首先经过 __getname
在内核缓冲区专用队列里申请一块内存用来放置路径名,其实这块内存就是 一个 4KB 的内存页。这块内存页是这样分配的,在开始的一小块空间放置结构体 struct filename 结构体前面字段的信息,这里咱们假设 iname 字段以前的结构使用 struct filename-iname 表示, 以后的空间放置字符串(保存在 iname)。初始化字符串指针 kname,使其指向这个字符串 (iname[]
)的首地址。而后就是拷贝字符串,返回值 len 表明了 已经 拷贝的字符串长度。若是这个字符串已经填满了内存页剩余空间,就说明该字符串的长度已经大于 4KB - (sizeof(struct filename-iname)了,这时就须要将结构体 struct filename-iname 从这个内存页中分离并单独分配空间,而后用整个内存页保存该字符串。
get_unused_fd_flags()
函数用来查找一个可用的 fd(文件描述符)。
int get_unused_fd_flags(unsigned flags) { return __alloc_fd(current->files, 0, rlimit(RLIMIT_NOFILE), flags); } 复制代码
/* * allocate a file descriptor, mark it busy. */ int __alloc_fd(struct files_struct *files, unsigned start, unsigned end, unsigned flags) { unsigned int fd; int error; struct fdtable * fdt; spin_lock(&files->file_lock); repeat: // 经过 files 字段获取 fdt 字段。(该函数考虑了线程竞争,较复杂不展开了。) fdt = files_fdtable(files); //从 start 开始搜索 fd = start; // 进程上一次获取的 fd 的下一个号(fd + 1)保存在 next_fd 中。因此从 next_fd 开始进行查找。 if (fd < files->next_fd) fd = files->next_fd; if (fd < fdt->max_fds) //获取下一个 fd fd = find_next_fd(fdt, fd); // N.B. For clone tasks sharing a files structure, this test // will limit the total number of files that can be opened. error = -EMFILE; if (fd >= end) goto out; // 获取 fd 后,判断是否须要扩展用来保存 file struct 描述符的数组(fdtable->fd)的容量。 // 返回 0 表示不须要,<0 表示错误,1 表示成功。 error = expand_files(files, fd); if (error < 0) goto out; // If we needed to expand the fs array we // might have blocked - try again. // 1,扩容成功,而且从新尝试获取fd // 由于扩容过程可能会发生阻塞,这期间就有可能其余线程也在获取 fd,因此前面获取的 fd // 可能被其余线程抢先占用了,由于 Linux 的唤醒是不保证顺序的。 if (error) goto repeat; if (start <= files->next_fd) files->next_fd = fd + 1; // 在 fdtable->open_fds 位图中置位表示当前获取的 fd 处于使用状态。 // 也就是说当释放该 fd 位图中对应的位清除,从而达到重复使用的的目的。 __set_open_fd(fd, fdt); // 若是设置了 O_CLOEXEC 标志,那么在 fdtable->close_on_exec 位图对应的位置位。 // 前面提到过开启 close-on-exc 标志,使用系统调用 execve() 装载程序的时候会关闭设置过该标志的文件。 // Linux 中使用 fork() 产生子进程的时候回继承父进程已打开的文件描述符集。execve() 通常就是在子进程 // 里用来运行新程序。 if (flags & O_CLOEXEC) __set_close_on_exec(fd, fdt); else __clear_close_on_exec(fd, fdt); // 设置返回值。 error = fd; #if 1 // Sanity check 一些合法性检查。 if (rcu_access_pointer(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 fdtable { unsigned int max_fds; struct file __ rcu ** fd; // current fd array unsigned long * close_on_exec; unsigned long * open_fds; unsigned long * full_fds_bits; struct rcu_head rcu; }; 复制代码
#ifdef CONFIG_64BIT #define BITS_PER_LONG 64 #else #define BITS_PER_LONG 32 #endif /* CONFIG_64BIT */ static inline void __set_open_fd(unsigned int fd, struct fdtable *fdt) { __set_bit(fd, fdt->open_fds); fd /= BITS_PER_LONG; if (!~fdt->open_fds[fd]) __set_bit(fd, fdt->full_fds_bits); } 复制代码
这里以 32 位 arm 芯片为例。其中函数 __set_bit
表示以某个地址开始在某个位置 1。 假设咱们目前数组的容量为 128 ,那么以下表:共有 4 行,一行 32 列,fd = 32 * row + column。 每一个格子中 0 表示当前 fd 没有被占用,1 表示占用了。其中 ...
表示全部的列为 1。 假设咱们如今获取的 fd 为 66 也就是第 3 行第 3 列,此时咱们能够看到该格子为 0。 调用 __set_bit(fd, fdt->open_fds);
把该位(66)置1,fd /= BITS_PER_LONG;
获取行号 66 / 32 = 2(行号从 0 开始),!~fdt->open_fds[fd]
, open_fds
为 long 类型 指针,也就是说步长为 32 位,至关于取第 3 个 long 数据的值,而后位取反,由于此时该 long 数据 的每一位都置 1 了,因此取反后的值为 0,!0 就为 true 了。此时咱们能够肯定第 3 行全部的 列都被使用了,因此咱们能够把 full_fds_bits
的第 2 位置 1,表示该行已所有被使用。
0 | 1 | 2 | ... | 30 | 31 |
---|---|---|---|---|---|
1 | 1 | 1 | ... | 1 | 1 |
1 | 1 | 1 | ... | 1 | 1 |
1 | 1 | 0 | ... | 1 | 1 |
1 | 1 | 1 | ... | 0 | 0 |
接下来看找 fd 函数 find_next_fd()
就很简单了。
static unsigned long find_next_fd(struct fdtable *fdt, unsigned long start) { unsigned long maxfd = fdt->max_fds; // 当前容量最后的一行 unsigned long maxbit = maxfd / BITS_PER_LONG; // 开始行 unsigned long bitbit = start / BITS_PER_LONG; // 先找到一个空行(有空闲位的某一行) bitbit = find_next_zero_bit(fdt->full_fds_bits, maxbit, bitbit) * BITS_PER_LONG; if (bitbit > maxfd) return maxfd; if (bitbit > start) start = bitbit; // 在该行上找到一个具体的空位。 return find_next_zero_bit(fdt->open_fds, maxfd, start); } 复制代码
尽可能以本身的能力对每行代码进行了注释,同时只是为了学习内核大神是如何玩转指针以及数据结构。 能够从 __set_open_fd()
函数看出对指针熟练的使用方式,以及快速定位的思想。