一、函数名称
a. int open(const char *pathname, int flags);
b. int open(const char *pathname, int flags, mode_t mode);
其中:pathname: 字符串类型的文件名称,如"/root/a.txt"
flags: 文件打开标志,以什么样的方式打开文件,为O_RDONLY、O_WRONLY、O_RDWR三者之一,是互斥的、不能同时存在;flags还可以是O_CREAT、O_APPEND等等,但上述三者之一可以与O_CREAT、O_APPEND等通过或(|)运算符连用。 所以内核hook_sys_open钩子中可以用if((flags & O_ACCMODE) == O_RDONLY) //判断是否以只读的方式打开,其中#define O_ACCMODE 3、 #define O_RDONLY 0、 #define O_WRONLY 1、#define O_RDWR 2
mode:访问权限,一定是flags标志为O_CREAT时,mode标识创建文件的访问权限。只有建立新文件时mode才会生效。其值:
S_IRWXU(即00700权限),代表该文件所有者具有可读 可写 可执行权限;
S_IRUSER或S_IREAD(00400权限),代表该文件所有者具有可读的权限;
。。。。
S_IRGRP(00040权限),代表该文件用户组具有可读的权限;
。。。。
S_IRWXO(00007权限),代表其它用户具有可读 可写 可执行的权限。
返回值:返回一个未使用的文件描述符(从3开始),因为0,1,2已经被stdin,stdout,stderr三个默认打开的文件占用,每个 进程都会默认打开这三个文件。文件描述符是有一个范围的:0 ~ OPEN_MAX-1 ,通过 ulimit
命令查看进程默认最多打开的文件数。
ulimit -n //结果1024
通过 ls -l /proc/{PID}/fd 可以查看某个进程打开了哪些文件:
c、openat
引入openat是方便一个进程内的各线程可拥有不同的当前目录,传统的chdir会影响整个进程,而使用openat只需要每个线程在初始化时打开一个目录(调用open),然后就可以以openat在“当前目录”操作文件了,如:使用open获得目录"/Users/zhanghuamao/unix/"文件描述符dirfd
将dirfd作为参数传入openat,pathname为./text.txt,即在/Users/zhanghuamao/unix/路径下创建了文件text.txt
int dirfd = open("/Users/zhanghuamao/unix/", O_RDONLY);
int filefd = openat(dirfd, "./text.txt", O_CREAT, S_IRUSR | S_IWUSR | S_IXUSR);
- openat比open多一个dirfd((文件描述符)的参数,dirfd表示需要进行open操作目录的文件描述符
- openat操作的文件路径为dirfd + pathname
例子:
(1)参数path指定为绝对路径名时,openat中参数dirfd会被忽略,openat函数就相当于open函数:
(2)path参数指定为相对路径名时,openat中的参数dirfd是通过打开相对路径名所在的目录来获取
(3)path参数指定了相对路径名,openat中的参数dirfd具有特殊值AT_FDCWD。在这种情况下,路径名在当前工作目录中获取,openat函数在操作上与open函数类似。
二、从用户进程的角度看待文件和文件系统
1、进程的工作目录
(1)进程的工作目录
进程在哪个路径下被运行起来哪个路径就是进程的工作目录(Current Woring Directory, CWD)。
比如,你在/home/mac下启动一个进程,那么该进程的工作目录就是/home/mac;如果你在/home/mac/bin下启动同一个程序,那么该进程的工作目录就变为/home/mac/bin。
(2)为什么要理解进程的工作路径?
理解了进程的工作目录就能知道为什么用代码进行文件操作的时候通常不写文件的绝对路径而只写文件名就可以了。
比如进程的工作目录是/home/mac,那么当创建一个叫做a.txt的文件时其实创建的是/home/mac/a.txt这个文件。也就是说其实我们可以简单的提供给进程一个相对路径,这个相对路径相对于谁呢,答案就是进程的工作路径。
(3)如何查看进程的工作路径
使用pwdx命令:pwdx pid
使用ll /proc/pid/cwd
2、进程与文件
(1)进程是通过文件描述符(file descriptors,简称fd)来访问文件的,而不是我们看到的文件名。
(2)每个进程都会管理一组自己打开的文件,如struct task_struct结构体所示,里面并没有对fd的直接描述,而是文件系统信息struct fs_struct、打开的文件信息struct files_struct、以及struct nsproxy命名空间。那么,我们将从这三个结构体逐步展开来理解进程是如何组织文件的。
2.1 进程中的文件系统信息
进程的文件系统信息是保存在进程描述符中的的struct fs_struct *fs成员中,其struct fs_struct定义在include/linux/fs_struct.h头文件中。
2.2 进程的文件信息
进程描述符的成员files类型为struct files_struct *,定义在include/linux/fdtable.h头文件中。该结构体表示进程打开所有文件的集合。
其中:《1》next_fd:表示下一次打开新文件时使用的文件描述符。
《2》close_on_exec_init:是位图,对执行exec时将关闭的所有文件描述符,在 close_on_exec 中对应的比特位都将置位。比特位数目刚好与NR_OPEN_DEFAULT一致。
《3》open_fds_init:是位图,open_fds_init 是最初的文件描述符集合。比特位数目刚好与NR_OPEN_DEFAULT一致。
《4》fd_array:fd_array的每个数组元素都是一个指针,指向每个打开文件的 struct file 实例。 默认情况下,内核允许每个进程打开 NR_OPEN_DEFAULT 个文件。该值定义在 include/
linux/sched.h 中,默认值为 BITS_PER_LONG,即在32位系统上,允许打开文件的初始数目是32,64位系统可以同时处理64个文件。如果一个进程试图同时打开更多的文件,内核必须对 files_struct中用于管理与进程相关的所有文件信息的各个成员,分配更多的内存空间。
《5》fdtab:最重要的信息包含在 fdtab 中;fdt指向自身成员fdtab或动态分配的struct fdtable实例。
文件描述符位图结构
full_fds_bits:每1bit代表的是一个64位的数组,也就是说代表了64个文件描述符;内核中的位图是一片连续的内存空间,最低bit表示数值0,下一比特表示1,依次类推;full_fds_bits每1bit只有0和1两个值,0表示有该组有可用的文件描述符,1表示没有可用的文件描述符,例如,位图bit 0代表的是0-63共64个文件描述符,bit1代表的是64-127共64个文件描述符,假如0-63文件描述符都被使用了,那么位图bit0则应该标记为1,如果64-127中有一个未使用的文件描述符,则bit1被标记为0,当64-127中的所有文件描述符都被使用的时候,才标记为1。
open_fds:是真正的文件描述符位图,也是一片连续的内存空间,每bit代表一个文件描述符(注意full_fds_bits每bit代表的是一组文件描述符),标记为0的bit表示该文件描述符没有被使用,标记为1的bit表示该文件描述符已经被使用,例如从内存其实地址开始计算,第35比特为1,则表示文件描述符35已经被使用了。
fd、max_fds:fd字段指向文件对象指针数组fd_array。该数组的长度存放在max_fds中,表示当前进程可打开的最大文件数。通常,fd字段指向files_struct的fd_array字段,该字段包含64个文件对象指针。如果进程打开的文件数目多于64个,内核就分配一个新的、更大的文件对象指针数组new_fd_array,并将其地址放在fd中,内核也同时更新max_fds字段的值。所以,进程的max_fds可能是64、128等。
补充—struct file结构体:
文件对象是描述文件和进程之间的关系,它表示进程已打开的文件。进程每打开一个文件,内核就创建一个文件对象,同一个文件会被不同的进程打开,就会创建不同的文件对象。即同一个文件在不同的进程中有不同的文件对象。
struct file{
union {
struct llist_node fu_llist;
struct rcu_head fu_rcuhead;
} f_u;
f_path 封装了两部分信息:文件名和inode之间的关联、文件所在文件系统的有关信息
struct path f_path;
struct inode *f_inode; /* cached value */
const struct file_operations *f_op;
/*
* Protects f_ep_links, f_flags.
* Must not be taken from IRQ context.
*/
spinlock_t f_lock;
enum rw_hint f_write_hint;
atomic_long_t f_count; //使用该文件对象的进程数
unsigned int f_flags; //指定打开该文件的方式,如O_RDONLY/O_NONBLOCK/O_SYNC
fmode_t f_mode; //保存创建文件时指定的文件模式,
//如FMODE_READ和FMODE_WRITE位来标识文件是否可读或可写
struct mutex f_pos_lock;
loff_t f_pos;
struct fown_struct f_owner; //包含了处理该文件的进程有关的信息
const struct cred *f_cred; //f_cred.uid、f_cred.gid指定了用户的UID和GID
struct file_ra_state f_ra;
/* needed for tty driver, and maybe others */
void *private_data;
struct address_space *f_mapping;
.......
}
在lsm的int security_file_open(struct file *file)中,检测用户打开文件的读写方式,如下:
相关结构图:
(1)struct files_struct在进程描述符内的展开。
《1》files_struct
files_struct: 表示进程打开所有文件的集合,next_fd表示下一个可能可用文件描述符,并不一定真正可用,假如0-10描述符都被使用了,中间释放文件描述符3,再打开文件,此时将使用3作为新的文件描述符,内核认为next_fd为4,next_fd只是表示可能可用的下一个文件描述符,下次查找可用描述符时从next_fd开始查找,而不需要从头开始找。
《2》fdtable
fdtable: 真正记录哪些文件描述符被使用了,哪些是空闲的,实际是一个文件描述符位图,每1bit表示了一个文件描述符,例如bit 0为1表示描述符0被使用了,bit 3为0表示描述符3可以使用。fd数组记录了file信息,数组下标就是文件描述符的值。
《3》file
file: 文件的真正信息,文件描述符只是个数组下标,通过下标查找file结构体信息,f_op记录的是文件读写及其他操作的真正函数,不同的文件系统,读写函数不一样,申请文件描述后,内核会将文件描述符与文件结构体(file读写函数等)关联起来。
(2)什么时候会出现多个进程的 fd 指向同一个struct file 结构体?
比如 fork 的时候,父进程打开了文件,后面 fork 出一个子进程。这种情况就会出现共享 file 的场景。如图:
(3)在同一个进程中,多个 fd 可能指向同一个 file 结构吗?
2.3 进程中文件系统挂载命名空间
三、内核服务例程
1、内核服务例程
2、do_sys_open函数
2.1 get_unused_fd_flags—获取一个未被使用的fd
前置知识:
Linux选择文件描述符是按从小到大的顺序进行寻找的,文件表中next_fd用于记录下一次开始寻找的起点。当有空闲的描述符时,即可分配。但当某个文件描述符关闭时,如果其小于next_fd,则next_fd就重置为这个描述符,这样下一次分配就会立刻重用这个文件描述符。以上的策略,总结成一句话就是“Linux文件描述符策略永远选择最小的可用的文件描述符”。
next_fd表示下一个可能可用的文件描述符,并不一定真正可用,下次查找可用描述符时从next_fd开始查找,而不需要从头开始找。假如0-10描述符都被使用了,中间释放了3这个文件描述符,再打开文件,此时将使用3作为新的文件描述符,内核认为next_fd为4,而4已经被使用了,因此有了函数 find_next_fd()。
bitmap记录已用、可用的文件描述符, max_fds 代表bit数,即当前可用的文件描述符
当分配的文件描述符大于 max_fds 的时候,会使用 expand_files() 同时拓展bitmap与数组fdt
fdt记录与fd相关的的 struct file 指针,它是数组指针
2.1.1 申请一个空闲的文件描述符,并标注为已使用
(1)从next_fd开始查找空闲的的文件描述符find_next_fd()
(2) 动态扩展文件描述符表fdtable
扩展文件描述符表fdtable
2.2 调用do_filp_open函数执行文件的打开过程,生成一个struct file