open
open(const char * path , flag , mode)
mode参数只在 create时才有用,既然都是c语言没有重载Linux下open函数是如何实现这个重载效果的呢?答案是可变参数。
open调用时内核做的事情
- 内核首先会对flags参数进行合法性检测
- 查找一个fd的值,这个fd的值是最小的还未使用的值在 fd_array 数组(文件描述符数组中)
- 申请一个 struct file , 申请 struct file 的时候,又会对 file中的 f_op赋值,给其赋上对应文件系统指定的操作函数。(毕竟Linux下一切皆文件,不同文件的操作函数不同,比如socket就与 常规文件不同)
- 将 fd 的值与 struct file 关联起来在 fd_array中
struct fdtable {
unsigned int max_fds; 表当前最大的文件描述符的值
struct file ** fd; /* current fd array */
fd_set *close_on_exec; execve 时需关闭的位图位图
fd_set *open_fds; 打开的文件描述符位图
struct rcu_head rcu;
struct fdtable *next; 扩容的时候它会指向下一个fdtable
};
struct files_struct {
/*
* read mostly part
*/
atomic_t count;//引用计数
struct fdtable *fdt;//默认指向 fdtab ,当需要扩容时指向新的fdtable
struct fdtable fdtab; // 用来快速查找文描的一个结构体
/*
* written part on a separate cache line in SMP
*/
spinlock_t file_lock ____cacheline_aligned_in_smp; //每次进行操作的文件锁
int next_fd;//指向下一个可用的fd
struct embedded_fd_set close_on_exec_init; // execve时close文描的位图
struct embedded_fd_set open_fds_init;//已经打开的文件描述符的位图
struct file * fd_array[NR_OPEN_DEFAULT];//struct file *的数组
//当默认的不够可以扩容,内核一开始使用默认的数组是为了避免每个进程都频繁的申请内存
};
open打开一个文件的常用技巧
int fd = 0;
do{
fd = open("abc.txt",O_RDWR);
if( fd == -1 && errno !=EINTR)
{
if(errno == ENOENT)
fd=open("abc.txt",O_RDWR|O_CREAT,0644);
else
perror("open"),exit(1);
}
}while(fd<0);
close
close函数用于把关闭对应的文件描述符。对应不同的文件类型有不同的操作。
close时内核做的事情
- 首先通过files_struct 得到 fdtable 。(得到具体的文件描述符表)
- 对fd 然后进行合法值检测
- 通过fdtable , 把fd 当下标快速找到对应文件的结构体。
- 找到后,先把fd_array中对应的位置的指针置空。
- 通过fdtable中的open 位图,把fd对应的位置为0。
- 设置好next_fd
- 检查 struct file 中的引用计数
- 如果为0,调对应文件系统的release函数对文件真真的关闭。
忘记调用close的后果
- 有一个无用的文件描述符占用位置
- 内核数据结构没有释放 , 造成的内存泄露(使用完后没有释放,导致的对应struct file结构体没有释放)
一个进程退出,会通过_exit系统调用退出。_exit系统调用会 对close所有打开的文件描述符,释放内核数据结构,将子进程过继给 init进程(孤儿进程)。
那么对于一个普通的程序来说是没有什么影响的,但是对于守护进程这种常驻的进程,就会造成资源的泄露。
快速查找进程是否文件泄露
lsof -p pid
read 与 write
内核做的事情
- 通过传入的 fd 得到对应的file结构体。
- 获取当前file结构体对应的位置偏移量
- 调用 vfs_read (通过内核 vfs 使用相应文件系统的fop 来进行操作)
- 首先会对 file 结构体查看是否是 read打开的。
- 然后查看对应文件系统是否支持读取操作。
- 查看应用层的 buff (我们传入的缓冲区) 是否可以写。
- 检查实际可以读取的字节数(所以read有的时候返回的值小于我们要求的值)
- 执行对应文件系统的函数。
read的部分读取
大多数read调用都是如果有数据读取上来立刻返回即使所读的数据小于要求的数据,这也就是部分读取。但是不同的文件系统的操作函数不同,对于socket的tcp套接字来说,是否读取部分数据返回,这却决于 阻塞 和 接受低水位线。
如果tcp 套接字是阻塞的并且tcp缓冲区的数据小于低水位线,那么read并不会读取部分数据立刻返回。
dup/dup2
dup内核
dup(int oldfd)
将oldfd 重定向到 最小最未使用的文件描述符上。(原有的oldfd并不会关闭)
1.先得到 oldfd 对应的file结构体。
2.调用get_unused_fd() 得到一个最小最未使用的文件描述符。
3.调用fd_install,将fd与 oldfd对应的file结构体相关联。
4.返回最小最未使用的文描
dup2
dup(int oldfd, int newfd)
1. 如果oldfd 与 newfd 相等 并且 oldfd 是有效的文描,那么立即返回
2. 如果不相等,查看newfd 是否需要扩展文件描述符表。
3. 释放 newfd对应的文描(close 掉 newfd)
4. 将 newfd 与 oldfd的文件结构体相关联(将oldfd 重定向到 newfd上)
这些操作都是线程安全的操作通过调用files_struct 中的锁。
文件数据的同步
sync
为什么要有数据同步,因为Linux是有内核缓冲区的,所有的读操作都是先把数据copy到内核缓冲区,然后从缓冲区中copy到应用层。所有的写操作也是先把数据copy到内核缓冲区,然后从内核缓冲区在写入到硬件中。
所以,我们平常调用write系统调用函数的时候,并不会立即把数据写到文件中,Linux内核会定期把脏数据从内核缓冲区中刷新到硬件中。
sync / fsync/fdatasync
sync 和 fsync 都是把文件的文件内容 和 元信息同步到硬件中,但是fdatasync 只同步文件内容 和 部分元信息,具体是否同步元信息,取决于是否该元信息数据会影响到后续的读取,列如 文件长度。
sync / fsync / fdatasync 都是阻塞调用,它们都会把内核数据刷新到硬件后才返回,但是fdatasync的性能优于前者在不要求同步所有元数据的前提下。
另外需要注意,即使刷到了硬件中,也不一定立刻写到硬件上(磁盘),因为硬件也有硬件缓存。
文件元信息
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode节点号 */
mode_t st_mode; /* 权限与文件类型 d/l/s/b/c/p/-(regular) */
nlink_t st_nlink; /* 硬链接的链接数 */
uid_t st_uid; /* 用户id该id在口令文件中对应着相应的用户 */
gid_t st_gid; /*用户组id*/
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /*文件总大小*/
blksize_t st_blksize; /* 该文件对于I/O时最合适的大小 */
blkcnt_t st_blocks; /* 被分配的block数目 */
time_t st_atime; /*访问时间*/
time_t st_mtime; /*最后一次修改文件内容的时间*/
time_t st_ctime; /*最后一次修改文件状态的时间,即访问inode节点的时间*/
};
stat/fstat/lstat
stat/fstat 与 lstat 这三个函数用来获得文件元信息,但是前俩个与后面的这个不同点在于,如何是个软链接文件,lstat 获得符号链接文件本身,而stat/fstat 获得符号链接指向的文件的元信息。
内核操作
- 申请一个struct stat , 调用vfs_stat 来获得元信息(又是vfs可见,linux通过 vfs来执行不同文件系统的操作)
- vfs_stat 找到对应文件的 dentry结构体最终调用 vfs_getattr。
- vfs_getattr 中,首先对获取操作进行安全检测(这块也不懂什么意思),然后通过 dentry找到对应文件的inode结构体。
- 通过inode的成员变量 i_op 来查看如果对应的文件系统有对应的获取文件元信息的操作就调用 文件系统自定义的操作函数,如果没有调用常规的。
常规的操作函数
void generic_fillattr(struct inode *inode, struct kstat *stat)
{
stat->dev = inode->i_sb->s_dev;
stat->ino = inode->i_ino;
stat->mode = inode->i_mode;
stat->nlink = inode->i_nlink;
stat->uid = inode->i_uid;
stat->gid = inode->i_gid;
stat->rdev = inode->i_rdev;
stat->size = i_size_read(inode);
stat->atime = inode->i_atime;
stat->mtime = inode->i_mtime;
stat->ctime = inode->i_ctime;
stat->blksize = (1 << inode->i_blkbits);
stat->blocks = inode->i_blocks;
}
文件权限
suid / sgid
uid / gid 标识出了当前 进程的用户是谁与用户组是那个用户组。 suid / sgid 则表明了当前进程 的权限id是谁。通常 suid/sgid 表明了 当前进程是否有权限是做什么。
因为在Linux下的权限分为, user/group/other。所以真正看该进程是否能做什么的时候,会检测 suid 与 该文件的uid是否相同,如果相同 当前进程有该文件的 User权限。对于Sgid 一样,也是检测 sgid 与 文件的 gid 是否相同,如果相同 表当前进程有该文件的 Group对应的权限操作。
所以一句话, uid/gid 表明你的身份是谁 , suid/sgid 表明你的权限。
访问inode时的权限检测函数
int inode_change_ok(const struct inode *inode, struct iattr *attr)
{
unsigned int ia_valid = attr->ia_valid;
……
/* Make sure a caller can chown. */
/* 只有在uid和suid都不符合条件的情况下,才会返回权限不足的错误*/
if ((ia_valid & ATTR_UID) && (current_fsuid() != inode->i_uid
||attr->ia_uid != inode->i_uid) && !capable(CAP_CHOWN))
return -EPERM;
……
}
从上可知只有 root / uid / suid ,三个占其中一个才有权限访问inode节点。
Stricky(权限粘滞位)
因为文件的删除只用看,父级目录是否有可执行权限与当前用户是否有写权限即可删除文件,所以并不安全。故有了Stricky,它只用来给目录设置,当设置后,只有root和该文件所有者有权限删除该文件。
文件截断
truncate / ftruncate
truncate ( const char * path , size )
ftruncate ( int fd , size)
为什么要文件截断
有的时候我们有这样的需求,我们想让守护进程每次打开一个文件的时候都希望该文件是一个空文件。这样守护进程就可以记录本次打开该文件后的一些最新的消息。
那么什么时候才能文件截断呢?只有该文件是常规文件并且拥有写权限的时候才能截断。
当文件大小 小于 size参数,文件大小会变大,扩展的部分为0,此时形成空洞文件,文件大小确实变大,但扩展的部分并未分配 block块。只有在相应空洞位置写入的时候,对应空洞位置的block块才会被分配,每在空洞位置处写入就会分配相应的块。具体块的分配跟文件系统格式化有关, 1k/2k/4k分配。