本篇主要介绍文件和文件系统中常用的一些函数,文件系统的组织结构和硬链接、符号链接。
通过对这些知识的了解,可以对Linux文件系统有更为全面的了解。
?
1 umask函数
之前我们已经了解了每个文件与权限相关的9个位(bit),我们现在来了解一下当每个进程创建文件时默认会设置该文件的文件权限(the file mode creation mask)。
umask函数设置该进程默认创建文件的权限掩码(the file mode creation mask),并且返回之前的权限掩码值。
#include <sys/stat.h>
mode_t umask(mode_t cmask);
the file mode creation mask的作用:当进程创建新文件时,会根据这个掩码值创建文件,在掩码值中打开的位,对应的新文件的权限位会被关闭。
umask的功能简单地说就是创建新文件时屏蔽掉用户不希望生效的权限位。
Example:
#include "apue.h"
#include <fcntl.h>
?
#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)
?
int
main(void)
{
? ? umask(0);
? ? if (creat("foo", RWRWRW) < 0)
? ? ? ? err_sys("creat error for foo");
? ? umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
? ? if (creat("bar", RWRWRW) < 0)
? ? ? ? err_sys("creat error for bar");
? ? exit(0);
}
运行截图:
结果说明:
shell的umask命令显示当前文件创建权限掩码。
0022表示创建出来的新文件,组用户和other用户没有写该文件的权限。
在程序中,首先用默认的权限设置创建了文件foo,它的权限位666,至于为什么当前用户也没有执行权限,我还没搞清楚,留一个坑 @suzhou。
然后修改umask值为0077,这样就屏蔽掉了组用户和其他用户的读写权限位,因此再创建新文件bar,它的权限位是600。
?
2 chmod、fchmod和fchmodat函数
函数作用:修改已有文件的权限位。
函数声明:
#include <sys/stat.h>
int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodeat(int fd, const char *pathname, mode_t mode, int flag);
?区别:
- chmod:作用在某个指定的文件上
- fchmod:作用在某个指定的已经打开的文件上
- fchmodat:类似之前的函数,当pathname为绝对路径,或者fd取值为AT_FDCWD并且pathname为相对路径时,fchmodat函数的作用和chmod相同;否则,工作目录为fd指定父目录路径加上pathname为父目录的子目录。参数flag用来指定fchmodeat的行为:取值为AT_SYMLINK_NOFOLLOW时,该权限修改并不追踪符号链接,只修改该符号链接文件的权限。
权限要求:要修改一个文件的权限,需要当前进程的effective user ID和文件的所有者ID相同,或者进程有超级用户权限。
代表各个权限位的常量如下表所示:
Example:
修改在上例中创建的两个文件foo和bar的权限。
#include "apue.h"
?
int
main(void)
{
? ? struct stat ? ? statbuf;
?
? ? /* turn on set-group-ID and turn off group-execute */
?
? ? if (stat("foo", &statbuf) < 0)
? ? ? ? err_sys("stat error for foo");
? ? if (chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
? ? ? ? err_sys("chmod error for foo");
?
? ? /* set absolute mode to "rw-r--r--" */
?
? ? if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0)
? ? ? ? err_sys("chmod error for bar");
?
? ? exit(0);
}
运行结果:
结果说明:
- 执行程序之前,foo的权限为666,程序中首先使用stat函数获取文件的信息存在statbuf中,置组用户执行位(S_IXGRP)为0,并置该位为S(S_ISGID)。
- 执行程序之前,bar的权限位600,程序中设置该文件的组用户和其他用户有读权限,设置成功之后,可以看到bar的文件权限位644.
高级权限(SUID/SGID/Sticky Bit )档案特殊权限 说明:
- SUID (Set UID): 会制作出 s 的权限,权限数字是 4 。这个权限只能作用于可执行文件,不可作用于目录,shell script也不行。使一般使用者在执行文件时,暂时具有该文件拥有者的权限。
- SGID (Set GID): 制作出 s 权限,权限数字是 2 。这个权限可以作用于目录和文件。作用于目录时,在该目录下建立的所有文件和目录的group都会是该目录的group;作用于文件时,则不论使用者是谁,在执行该程序的时候, 他的有效群组 (effective group) 将会变成该程序的群组所有人 (group id)。
- Sticky Bit: 制作出 t 权限,权限数字是 1 。这个权限只能作用于目录。在该权限作用的目录里,如果使用者具有w,x权限,那么他所创建的文件和目录,只有档案的拥有者和root才能删除。
- 注意:在字符表示权限时,s和t是出现在x的位置的,意思是在可执行的文件和目录时,它们才是有意义的。如果文件或目录本身u、g或o就没有可执行权限,那么它所在的权限组出现的是S或者T,表示空的意思。用数字表示权限,就是在rwx前面加上4、2、1的组合就是了,如1777。
?
3 Sticky Bit(S_ISVTX)
Sticky bit有一个冗长的历史,这里并不赘述。
作用:如果文件夹的sticky bit被打开,则文件夹中的文件被删除或者重命名需要满足一下的条件之一:当前用户对该文件夹有写权限;当前用户是该文件的所有者;当前用户是该文件夹的所有者;当前用户是超级用户。
典型的打开了sticky bit的两个文件夹是/tmp 和/var/tmp,这两个文件夹对所有用户都有读写和执行权限,这样所有用户都可以在该文件夹下创建文件,但是并不能删除由其他用户所有的文件。
?
4 chown、fchown、fchownat和lchown函数
函数作用:修改文件所属的用户ID和组ID,如果参数owner和group有一个的值为-1,对应的ID值不变。
#include <unistd.h>
int chown(const char* pathname, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int fchownat(int fd, const char* pathname, uid_t owner, gid_t grou, int flag);
int lchown(const char* pathname, uid_t owner, gid_t group);
区别:
当目标文件不是一个符号链接时,上面的四个函数的作用基本相同。
当目标文件是一个符号链接文件时,并且函数lchown和fchownat的flag设置为AT_SYMLINK_NOFOLLOW时,这两个函数修改符号链接文件本身的所有者,而不是符号链接指向的文件。
fchown函数修改fd代表的已打开文件的所有者。因为fchown操作的是已经打开的文件,所以它不可以用来修改符号链接的所有者。
fchownat函数的行为:当参数pathname是绝对路径,或者参数fd的值为AT_FDCWD并且pathname为相对路径时,fchownat函数的行为和chown和lchown类似;具体来说,flag取值AT_SYMLINK_NOFOLLOW时,行为和lchown类似;否则行为和chown类似。当fd代表已打开的文件夹,pathname为一个相对路径时,目标文件为以fd为父目录,pathname为相对的子目录所指的文件。
?
5 文件大小(File Size)
stat结构体中的成员st_size表示文件大小字节数。
字段st_size只对常规文件(regular files)、目录文件(directories)和符号链接文件(symbolic links)。
更多的细节:
- 常规文件大小允许为0;
- 目录文件的大小往往是整数,如16或512;
- 符号链接文件的大小是被链接文件名的长度,如符号链接lib—>usr/lib,大小为7,正好是字符串“usr/lib”长度,并不需要像C风格字符串一样+1。
?
6 文件截断(File Truncation)
有时候我们需要通过丢弃文件结尾一部分来截短文件。
函数声明:
#include <unistd.h>
int truncate(const char* pathname, off_t length);
itn ftruncate(int fd, off_t length);
函数作用:把目标文件截断到参数length指定的长度。
参数:如果参数length的值比指定文件的长度小,则文件超过length长度的部分不可读;如果length的值比指定文件的长度大,文件长度扩大到length,扩充部分填充0(可能是一个hole)
?
7 文件系统(File Systems)
为了理解文件链接,我们需要对Unix文件系统的结构有一个概念性地认识。
一个磁盘(disk drive)可以被分为多个分区(partition),每个分区都可以包含一个文件系统(file system)。
inode是一个定长的索引,包含了一个文件的大部分信息。
关于 i-nodes和data blocks的内存布局如下图所示:
说明:
- 两个目录索引(directory entry)指向同一个i-node索引(i-node entry)。即多个文件夹包含同一个文件(可能通过硬链接或软链接),每一个inode都由一个字段link count,包含指向该i-node的目录索引数。只有当该数值(link count)为0时,该文件才会被删除(释放文件所占用的data blocks)。索引后面介绍的移除目录的函数不叫delete,而是unlink。在stat数据结构中,link count对应的字段为st_nlink。这里提到的链接为硬链接。
- 另外一种链接为符号链接。符号链接文件对应的data block中存有该链接指向文件的文件名(之前有提到)。对应的i-node中文件类型字段的值应为S_IFLINK,告诉文件系统当前文件为符号链接。
- i-node包含了对应文件的所有信息:如文件类型,指向文件数据块的指针,文件权限位,文件大小等。stat数据结构中得大部分字段都是从i-node中获取。有两个字段值存在目录索引中:文件名(file name)和i-node数(i-node number)。i-node数目对应的数据类型为ino_t。
我们再了解一下目录文件的link count字段。
当我们执行下面的命令:
mkdir testdir
?内存中数据组织结构如下所示:
从上图可以看到,目录下每多加一个文件或文件夹,link count都会加1。
图中显示的都很清楚,不做过多赘述(其实是我懒了,哈哈...)
?
8 link、linkat、unlink、unlinkat和remove函数
从前面可以知道,多个目录索引可以指向同一个文件,即多个目录可以包含同一个文件。
link和linkat的作用是创建一个已存在文件的链接。
函数声明:
#include <unistd.h>
int link(const char* existingpath, const char* newpath);
int linkat(int efd, const char* existingpath, int nfd, const char* newpath, int flag);
函数细节:
- 函数创建一个新目录索引newpath,关联到已存在文件existingpath;
- 如果newpath已经存在,返回错误;
- newpath只有最后一个文件名会被创建,路径其他部分必须已存在;
- linkat的参数的作用和前面xxxat函数介绍过的类似,这里不再赘述;
- 如果被链接的文件是一个符号链接文件,参数flag用来控制当前新建的链接是关联到该符号链接(默认行为),还是该链接指向的文件(AT_SYMLINK_FOLLOW);
- 一般来说不可以扩文件系统创建链接,防止文件系统间的硬链接循环;
下面再了解一下unlink函数。
函数声明:
#include <unistd.h>
int unlink(const char* pathname);
int unlinkat(int fd, const char* pathname, int flag);
函数细节:
- 函数会删除目录索引,并且对应文件的inode的link count减1,如果link count不为0,则说明还有其他目录索引指向该文件,不删除该文件,否则删除该文件;
- 进程必须对该目录有写权限和执行权限;
- 如果pathname是一个符号链接,unlink移除该符号链接文件,而不是该符号链接指向的实体文件。没有函数实现直接删除实体文件这个功能。
和unlink类似功能的函数remove:
#include <stdio.h>
int remove(const char* pathname);
函数细节:
- remove一个文件相当于调用unlink,remove一个文件夹相当于调用rmdir(后面会介绍)。
?
9 rename和renameat函数
函数声明:
#include <stdio.h>
int rename(const char* oldname, const char* newname);
int renameat(int oldfd, const char* oldname, int newfd, const char* newname);
函数细节:
- 如果oldname不是一个目录路径,说明我们正在重命名一个文件或符号链接。这里newname已存在,则不可以是一个已存在目录的名字。如果newname已存在并且不是目录名,则删除已有文件newname,将oldname所指文件重命名为newname。权限要求:我们必须有包含oldname和newname文件的目录的写权限。
- 如果oldname是一个目录路径,则我们在重命名一个目录。如果newname已存在,它必须是一个目录的名字,并且该目录必须是空目录(只有. 和 ..索引)。这种情况下,删除已存在目录newname,将待重命名目录oldname重命名为newname。还需要注意的,newname不能包含oldname。例如:不能将目录/usr/foo重命名为/usr/foo/testdir,因为/usr/foo是新目录名的前缀,无法被删除。
- 如果oldname或newname为一个符号链接名,那么被重命名的时符号链接本身,不是符号链接指向的文件。
- 我们不能重命名dot(.)和dot-dot(..),更确切地说,这两个特殊名不能作为路径名的最后一部分。
- 如果oldname和newname相同,则函数返回成功,不做任何事情。
对renameat函数的参数含义不再赘述。
?
10 符号链接(Symbolic Links)
符号链接可以认为是文件的非直接指针,而硬链接(hard link)可以看做是文件的直接指针,因为它直接指向文件的inode。
- 硬链接通常不可以跨文件系统;
- 只有超级用户可以对一个目录创建硬链接。
符号链接并没有文件系统间的限制。
由于符号链接的存在,我们在操作文件或目录时,总是需要知道该文件是否是一个符号链接,然后判断该函数的操作对象是符号链接指向的文件还是符号链接本身。
下图总结了一些常用函数是否追踪链接,仅供参考:
Example:
链接情况如下图所示:
可以发现,由于符号链接而出现了一个环。
再来试验一下:
我们发现,这样会无限循环下去。
解除循环的方法就是使用unlink删除该符号链接。需要注意的一点是,如果这里出现的链接是硬链接,则删除这个循环链接会困难得多。因此只有管理员权限才可以用link函数对一个文件建立硬链接。
有时候符号链接会造成一些迷惑的事情,如果使用者对符号链接不熟悉的话。
如下所示:
符号链接可以链接一个不存在的文件,如果使用者对符号链接不熟悉,则很容易被这种情况迷惑。
这时,可以使用ls的”-l"命令看到有 “->"符号,或者前面符号位第一位的”l”表示符号链接,或者用ls得“-F”命令,符号链接文件后面会跟一个@符。
?
11 创建和读取符号链接(Symbolic Links)
函数作用:创建符号链接
函数声明:
#include <unistd.h>
int symlink(const char *actualpath, const char* sympath);
int symlinkat(const char* actualpath, int fd, const char* sympath);
参数说明:
- 函数并不要求actualpath一定存在,在上面的例子中也看到了这一点。
因为open函数会打开符号链接指向的实际文件,所以需要一个函数打开符号链接本身。
函数声明:
#include <unistd.h>
ssize_t readlink(const char* restrict pathname, char* restrict buf, size_t bufsize);
ssize_t readlinkat(int fd, const char* restrict pathname, char* restrict buf, size_t bufsize);
如果调用成功,函数从符号链接中读取buf长的内容。
?
?
参考资料:
《Advanced Programming in the UNIX Envinronment 3rd》
?