文件和目录
函数stat、fstat、fstatat和lstat
- 4个
stat函数
主要用于返回文件的信息结构
#include
int stat(const char *restrict pathname, struct stat *restrict buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *restrict pathname, struct stat *restrict buf);
int fstatat(int fd, const char *restrict pathname, struct stat *restrict buf, int flag);
-
stat函数
返回文件有关的信息结构 -
fstat函数
获得已在描述符fd
上打开文件的有关信息 -
lstat函数
与stat函数
类似,但是在文件是一个符号链接时,返回该符号链接的有关信息,而不是由符号链接引用的文件的信息 -
fstatat函数
为一个相对于当前打开目录(由fd参数指向)的路径名返回文件统计信息-
flag
参数控制是否跟随一个符号链接- 当
flag
设置为AT_SYMLINK_NOFOLLOW
时,fstatat
不会跟随符号链接,而是返回符号链接本身的信息 - 默认情况下,,返回的是符号链接指向的实际文件的信息
- 如果
fd参数
的值是AT_FDCWD
,并且pathname
是一个相对路径名,则会根据相对路径返回信息(cwd :current work directory
) - 如果
pathname
是一个绝对路径,忽略fd参数
- 当
-
-
macos
下stat定义
struct stat {
dev_t st_dev; /* [XSI] ID of device containing file */
ino_t st_ino; /* [XSI] File serial number */
mode_t st_mode; /* [XSI] Mode of file (see below) */
nlink_t st_nlink; /* [XSI] Number of hard links */
uid_t st_uid; /* [XSI] User ID of the file */
gid_t st_gid; /* [XSI] Group ID of the file */
dev_t st_rdev; /* [XSI] Device ID */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
struct timespec st_atimespec; /* time of last access */
struct timespec st_mtimespec; /* time of last data modification */
struct timespec st_ctimespec; /* time of last status change */
#else
time_t st_atime; /* [XSI] Time of last access */
long st_atimensec; /* nsec of last access */
time_t st_mtime; /* [XSI] Last data modification time */
long st_mtimensec; /* last data modification nsec */
time_t st_ctime; /* [XSI] Time of last status change */
long st_ctimensec; /* nsec of last status change */
#endif
off_t st_size; /* [XSI] file size, in bytes */
blkcnt_t st_blocks; /* [XSI] blocks allocated for file */
blksize_t st_blksize; /* [XSI] optimal blocksize for I/O */
__uint32_t st_flags; /* user defined flags for file */
__uint32_t st_gen; /* file generation number */
__int32_t st_lspare; /* RESERVED: DO NOT USE! */
__int64_t st_qspare[2]; /* RESERVED: DO NOT USE! */
};
文件类型
-
普通文件
- 文本还是二进制数据,对于
UNIX内核
来说并无区别 - 二进制可执行文件必须遵循一种标准化的格式,以便内核理解
- 文本还是二进制数据,对于
-
目录
- 目录文件包含其他文件的名字以及指向与其他文件有关信息的指针
- 任一对指定目录具有读权限的进程都可以读目录的内容,但只有内核可以写目录文件
-
块特殊文件
- 提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行
-
字符特殊文件
- 提供对设备不带缓冲的访问,每次访问长度可变
- 系统中的设备要么是字符特殊文件,要么是块特殊文件
-
FIFO
- 用于进程间通讯,也称为
命名管道(named pipe)
- 用于进程间通讯,也称为
-
套接字(socket)
- 进程间的网络通讯,也可用在一台宿主机上进程之间的非网络通信
-
符号链接
- 这种类型的文件指向另一个文件
#define S_ISBLK(m) (((m) & S_IFMT) == S_IFBLK) /* block special 块特殊文件*/
#define S_ISCHR(m) (((m) & S_IFMT) == S_IFCHR) /* char special 字符特殊文件*/
#define S_ISDIR(m) (((m) & S_IFMT) == S_IFDIR) /* directory 目录文件*/
#define S_ISFIFO(m) (((m) & S_IFMT) == S_IFIFO) /* fifo or socket 管道或FIFIO*/
#define S_ISREG(m) (((m) & S_IFMT) == S_IFREG) /* regular file 普通文件*/
#define S_ISLNK(m) (((m) & S_IFMT) == S_IFLNK) /* symbolic link 符号链接*/
#define S_ISSOCK(m) (((m) & S_IFMT) == S_IFSOCK) /* socket 套接字*/
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define S_ISWHT(m) (((m) & S_IFMT) == S_IFWHT) /* OBSOLETE: whiteout */
#endif
-
POSIX.1
允许实现将进程间通信(IPC)对象
(如消息队列和信号量等)说明为文件。这些宏与上面的不同,它们的参数不是st_mode
,而是指向stat结构
的指针
#define S_TYPEISMQ(buf) (0) /* Test for a message queue 消息队列 */
#define S_TYPEISSEM(buf) (0) /* Test for a semaphore 信号量*/
#define S_TYPEISSHM(buf) (0) /* Test for a shared memory object 共享存储对象*/
- 示例:打印文件类型
#include
#include
int main(int argc, char *argv[]) {
int i;
struct stat buf;
char *ptr;
for (i = 1; i < argc; i++) {
printf("%s: ", argv[i]);
if (lstat(argv[i], &buf) < 0) {
err_ret("lstat error");
continue;
}
if (S_ISREG(buf.st_mode))
ptr = "regular";
else if (S_ISDIR(buf.st_mode))
ptr = "directory";
else if (S_ISCHR(buf.st_mode))
ptr = "charactor special";
else if (S_ISBLK(buf.st_mode))
ptr = "block special";
else if (S_ISFIFO(buf.st_mode))
ptr = "fifo";
else if (S_ISLNK(buf.st_mode))
ptr = "symbolic link";
else if (S_ISSOCK(buf.st_mode))
ptr = "socket";
else
ptr = "** unknown mode **";
printf("%s\n", ptr);
}
exit(0);
}
- 示例测试及输出
$ ./filedir /etc/passwd /etc /dev/tty /dev/disk4 /dev/fd
/etc/passwd: regular
/etc: symbolic link
/dev/tty: charactor special
/dev/disk4: block special
/dev/fd: directory
设置用户ID以及设置组ID
-
与一个进程有关的
ID
关联 ID
作用 实际用户ID
实际组ID
我们实际上是谁
这两个字段在登录时取自口令文件中的登录项有效用户ID
有效组ID
附属组ID
用于文件访问权限检查 保存的 设置用户ID
保存的设置组ID
由 exec函数
保存
在执行一个程序时包含了有效用户ID
和有效组ID
的副本 每一个文件有一个
所有者(st_uid)
和组所有者(st_gid)
(在stat结构
中指定)-
st_mode
中的设置用户ID位(S_ISUID)
和设置组ID位(S_ISGID)
-
设置用户ID(set-user-ID)
:当执行此文件时,将进程的有效用户ID
设置为文件所有者的用户ID
-
设置组ID(set-group-ID)
:将执行此文件的进程有效组ID
设置为文件的组所有者ID
-
文件访问权限
-
所有类型的文件都有访问权限位
s_mode
屏蔽含义 S_IRUSR
S_IWUSR
S_IXUSR
用户读
用户写
用户执行S_IRGRP
S_IWGRP
S_IXGRP
组读
组写
组执行S_IROTH
S_IWOTH
S_IXOTH
其他读
其他写
其他执行 -
常见使用方式
- 用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录,都应该具有执行权限。(目录的执行权限位常被称为搜索位)
- 例如打开
/usr/include/stdio.h
,需要对目录/
、/usr
和/usr/include
具有执行权限 - 目录的读权限仅仅允许获得该目录中所有文件名的列表,如果不具备执行权限,则无法找到需要的文件
- 例如打开
- 对一个文件的读权限决定了能否打开现有文件进行读操作
- 对一个文件的写权限决定了能否打开现有文件进行写操作
- 为了删除一个现有文件,必须对包含该文件的目录具有写权限和执行权限,对该文件本身不需要具备读、写权限
- 用
exec函数
执行某个文件,都必须对该文件具有执行权限,且该文件还必须是一个普通文件
- 用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录,都应该具有执行权限。(目录的执行权限位常被称为搜索位)
-
进程每次对文件进行操作时,内核就进行文件访问权限测试
- 若进程的
有效用户ID
是0(超级用户)
,则允许访问 - 若进程的
有效用户ID
等于文件的所有者ID
(进程拥有此文件),如果有适当的访问权限(例如只读权限的文件就不能写),则允许访问 - 若进程的
有效组ID
或进程的附属组ID
之一等于文件的组ID
,如果有适当访问权限,则允许访问 - 若其他用户适当的访问权限被设置,则允许访问
- 若进程的
新文件和目录的所有权
- 新文件的
用户ID
设置位进程的有效用户ID
-
POSIX.1
允许实现选择下列之一作为新文件的组ID
-
新文件的组ID
可以是进程的有效组ID
-
新文件的组ID
可以是它所在目录的组ID
-
函数access和faccessat
内核以进程的
有效用户ID
和有效组ID
为基础进行访问权限测试,有时,进程也希望按其实际用户ID
和实际组ID
来测试其访问能力-
access
和faccessat
函数按照实际用户ID
和实际组ID
进行访问权限测试的#include
int access(const char *pathname, int mode); int faccessat(int fd, const char *pathname, int mode, int flag); - 如果测试文件是否已经存在,
mode=F_OK
,否则,mode
是测试读(R_OK
)、写(W_OK
)、执行(X_OK
)权限常量的按位或 -
pathname
是绝对路径,则access
与faccessat
相同 - 如果
fd=AT_FDCWD
,且pathname
为相对路径,则以相对路径测试访问权限 -
flag参数
可以改变faccessat
的行为,flag=AT_EACCESS
,则访问检查用的调用进程的有效用户ID
和有效组ID
#include
#include #include int main(int argc,char *argv[]){ if(argc!=2) err_quit("usage: your_exec "); if (access(argv[1],R_OK)<0) err_ret("access error for %s",argv[1]); else printf("read access OK\n"); if(open(argv[1],O_RDONLY)<0) err_ret("open error for %s",argv[1]); else printf("open for reading OK\n"); exit(0); } - 如果测试文件是否已经存在,
函数umask
-
umask函数
为进程设置文件模式(mode_t
)创建屏蔽字,并返回之前的值#include
mode_t umask(mode_t cmask); -
cmask
是若干文件访问权限位(S_IRUSR
、S_IWUSR
等)按位“或”构成的
-
-
示例
#include
#include #include #define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH) int main() { 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); } - 可以查看创建的文件的权限
(base) yuqdeiMac:4_file_dir yuq$ ls -l bar foo -rw------- 1 yuq staff 0 Jun 10 17:42 bar -rw-rw-rw- 1 yuq staff 0 Jun 10 17:42 foo
更改进程的文件模式创建字并不影响其父进程(通常是
shell
)的屏蔽字,所有shell
都有内置umask命令
-
终端中
umask命令
可以输出当前屏蔽字(8进制显示,umask -S
以符号格式输出屏蔽字)$ umask 0022 $ umask -S u=rwx,g=rx,o=rx
函数chmod、fchmod和fchmodat
-
3个函数主要用于更改现有文件的权限
#include
int chmod(const chat *pathname, mode_t mode); int fchmod(int fd, mode_t mode); int fchmodat(int fd, const char *pathname, mode_t mode, int flag); chmod
在指定的文件上进行操作fchmod
对已打开的文件进行操作-
fchmodat
在两种情况下与chmod相同-
pathname
是绝对路径 -
fd
取值为AT_FDCWD
,pathname
为相对路径 - 否则,计算相对于打开目录(
fd
指定)的pathname
-
-
flag
参数改变fchmodat
的行为- 当
flag=AT_SYMLINK_NOFOLLOW
,fchmodat
不会跟随符号链接
- 当
-
为了改变一个文件的权限位
进程的有
效用户ID
必须等于文件的所有者ID
-
或者该进程必须拥有
root权限
mode
说明 S_ISUID
S_ISGID
S_ISVTX
执行时 设置用户ID
执行时设置组ID
保存正文(粘着位)S_IRWXU
S_IRUSR
S_IWUSR
S_IXUSR
用户读、写、执行
用户读
用户写
用户执行S_IRWXG
S_IRGRP
S_IWGRP
S_IXGRP
组读、写、执行
组读
组写
组执行S_IRWXO
S_IROTH
S_IWOTH
S_IXOTH
其他读、写、执行
其他读
其他写
其他执行
-
示例
#include
#include int main(){ struct stat statbuf; 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"); if(chmod("bar",S_IRUSR|S_IWUSR|S_IRGRP|S_IROTH)<0) err_sys("chmod error for bar"); exit(0); } -
执行后,检查文件权限
$ ls -l foo bar -rw-r--r-- 1 yuq staff 0 Jun 10 17:42 bar -rw-rwSrw- 1 yuq staff 0 Jun 10 17:42 foo
-
ls
列出的时间和日期并没有改变,chmod函数
更新的是i节点
最近一次被更改的时间,默认情况下,ls -l
列出的是最后修改文件内容的时间
-
-
chmod函数
在下列条件下自动清除两个权限位-
Solaris
等系统对用于普通文件的粘着位(S_ISVTX)
赋予了特殊含义,在这些系统上如果我们试图设置粘着位且没有root权限
,那么mode
中的粘着位自动被关闭。- 这意味着只有
root用户
才能设置普通文件的粘着位
- 这意味着只有
- 新创建文件的
组ID
可能不是调用进程所属的组。如果新文件的组ID
不等于进程的有效组ID
或者进程的附属组ID
中的一个,而且进程没有root权限
,那么设置组ID
位会被自动关闭。- 新文件的
组ID
可能是父目录的组ID
- 防止了用户创建一个
设置组ID
文件,而该文件是由并非该用户所属的组拥有的
- 新文件的
-
粘着位
- 在
UNIX
的早期版本中,S_ISVTX
粘着位将程序正文部分(机器指令)的一个副本保存在交换区中,是的下次执行该程序时能较快的将其装载入内存- 原因:通常的UNIX系统中,文件的数据块很可能是随机存放的,相比较而言,交换区是被作为一个连续文件来处理的
- 对于交换区可以同时存放的设置粘着位的文件数是有限制的,以免过多的占用交换区空间
- 后来的
UNIX
版本称它为保存正文位(saved-text bit) - 现今较新的UNIX系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术
-
Single UNIX Specification
允许针对目录设置粘着位- 如果对一个目录设置了粘着位,则只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
- 1.拥有此文件;2.拥有此目录;3.是超级用户
root
- 1.拥有此文件;2.拥有此目录;3.是超级用户
- 如果对一个目录设置了粘着位,则只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
函数chown、fchown、fchownat和lchown
-
用于更改文件的
用户ID
和组ID
#include
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, git_t group, int flag); int lchown(const char *pathname, uid_t owner, gid_t group); 除了所引用的文件是
符号链接
以外,这4个函数的操作类似在符号链接情况下,
lchown
和fchownat
(设置了flag=AT_SYMLINK_NOFOLLOW
标志)更改符号链接本身的所有者,而不是其指向的文件的所有者fchown函数
改变fd
指向的打开文件的所有者,既然它在一个打开的文件上操作,那么它就不能用于改变符号链接的所有者-
fchownat函数
与chown
或者lchown函数
在下面两种情况下是相同的-
pathname
是绝对路径 -
fd参数
取值为AT_FDCWD
而pathname
参数为相对路径 - 在上面两种情况下,如果设置了
flag=AT_SYMLINK_NOFOLLOW
标志,fchownat
与lchown
行为相同
-
-
_POSIX_CHOWN_RESTRICTED
常量可选的定义在头文件
中,而且总是可以用pathconf
或fpathconf
函数进行查询。此选项还与所引用的文件有关——可在每个文件系统的基础上,使该选项起作用或不起作用- 1.只有超级用户才能更改一个文件的所有者
- 2.允许任一用户更改他们所拥有的文件的所有者
-
若
_POSIX_CHOWN_RESTRICTED
对指定的文件生效- 只有超级用户进程才能更改该文件的
用户ID
- 如果进程拥有该文件(
有效用户ID
等于该文件的用户ID
),参数owner
等于-1
或文件的用户ID
,并且参数group
等于进程的有效组ID
或进程的附属组ID
之一,那么一个非超级用户进程可以更改该文件的组ID
- 只有超级用户进程才能更改该文件的
当
_POSIX_CHOWN_RESTRICTED
有效时,不能更改其他用户文件的用户ID
。可以更改你所拥有的文件的组ID
,但只能改到你所属的组如果这些函数由非超级用户进程调用,则在成功返回时,该文件的
设置用户ID位
和设置组ID位
都将被清除
文件长度
-
stat结构
成员st_size
表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义。(部分系统对管道也定义了文件长度,表示从管道中读取的字节数) - 对于目录,文件长度通常是一个数(如16或512)的整数倍
- 对于符号链接,文件长度是在文件名中的实际字节数(注意,因为符号链接的文件长度总是由
st_size
指示,所以它并不包含通常C语言用作名字结尾的null字节) - 大多数现代
UNIX
系统提供字段st_blksize
和st_blocks
-
st_blksize
:对I/O
较合适的块长度(前面曾经提到过,将st_blksize
用于读操作时,读一个文件所需的时间量最少) -
st_blocks
:所分配的实际512字节(不同系统大小不一定一样)块块数
-
- 文件中的空洞
-
ls -l 文件名
:输出的文件长度包含了空洞 -
du -s 文件名
:输出实际占用的字节块数量 -
wc -c 文件名
:计算文件中的字符数(字节),正常的I/O操作
读到的是整个文件的长度,包含了空洞 -
cat 文件名 > 副本文件名
:复制文件,原文件中的空洞会被自动填充(填充0)
-
文件截断
打开文件时使用
O_TRUNC标志
可以将一个文件长度截断为0
-
截断文件可以调用函数
truncate
和ftruncate
#include
int truncate(const char *pathname, off_t length); int ftruncate(int fd, off_t length); - 如果文件以前的长度大于
length
,则length
以外的数据就不再能访问 - 如果文件以前的长度小于
length
,则文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)
- 如果文件以前的长度大于
文件系统
传统的基于
BSD
的UNIX文件系统(UFS)
,以Berkeley快速文件系统
为基础可以把磁盘分成一个或多个分区。每个分区可以包含一个文件系统。
-
i节点
是固定长度的记录项,它包含有关文件的大部分信息
-
更仔细的观察一个柱面组的i节点和数据块部分,可以看到下图所示
- 有两个目录块指向同一个
i节点
。每个i节点
都有一个链接计数,表明指向该i节点
的目录项数。只有当链接计数为0时,才能删除该文件- 所以“解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”
- 所以删除一个目录项的函数被称之为
unlink
而不是delete
-
stat结构
中,st_nlink
代表链接计数,其基本数据类型是nlink_t
,这种链接类型称为硬链接 -
POSIX.1
中LINK_MAX常量
指定一个文件链接数的最大值
- 另外一种链接类型称为
符号链接
。符号链接文件的实际内容包含了该符号链接所指向的文件的名字。其i节点
中的文件类型是S_IFLNK
,于是系统知道这是一个符号链接 -
i节点
包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件数据块的指针等。stat结构
中的大部分信息取自i节点
。只有两项重要数据存放在目录项中:文件名
和i节点编号
(数据类型ino_t
) - 目录项中的
i节点编号
指向同一文件系统中的相应i节点
,一个目录项不能指向另一个文件系统的i节点
(所以硬链接ln
不能跨文件系统) - 当在不更换文件系统的情况下,为一个文件重命名时,该文件的实际内容并未移动,只需构造一个指向现有
i节点
的新目录项,并删除老的目录项
- 有两个目录块指向同一个
-
目录文件的链接计数
- 编号2549的
i节点
是一个目录,链接计数为2.任何一个叶目录(不包含其他目录的目录)的链接计数总是2,一个是命名(testdir
)该目录的目录项,另一个是.
项 - 新建目录下自动生成的
..
项指向该目录的父目录,父目录的i节点
链接计数加1
- 编号2549的
函数link、linkat、unlink、unlinkat和remove
-
任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法是使用link函数或linkat函数
#include
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和之前类似
- 如果任一路径existingpath或者newpath为绝对路径,则对用的文件描述符无效(efd、nfd)
- 如果efd或nfd设置为AT_FDCWD,则根据当前目录进行计算
- 如果路径是相对路径,根据相对于对应文件描述符进行计算
- 当现有文件是符号链接时,有flag参数控制linkat函数是创建指向现有符号链接的链接还是创建指向现有符号链接所指向的文件的链接。
- 如果flag=AT_SYMLINK_FOLLOW,则创建指向符号链接目标的链接
- 创建新目录项和增加链接计数应该是一个原子操作
- POSIX.1允许实现支持跨越文件系统的链接,但是大多数实现要求现有的和新建的两个路径名在同一个文件系统中。
- 如果实现支持创建指向一个目录的硬链接,那么也仅限于超级用户才可以这么做。
- 因为可能在文件系统中形成循环(想象你在test1目录下创建了test2目录,且test2目录指向test1目录,读取test1目录下的的所有文件时(包括子目录下的文件),又会通过test2读取test1,循环往复)
-
使用unlink函数删除一个现有目录项
#include
int unlink(const char *pathname); int unlinkat(int fd, const char *pathname, int flag); - 这两个函数删除目录项,并将由
pathname
所引用文件的链接计数减1,只有链接计数为0时,该文件的内容才可被删除,如果有进程打开了该文件,其内容也不能删除 - 为了解除对文件的引用,必须对包含该目录项的目录具有写和执行权限
- 如果对该目录设置了粘着位,则对该目录必须具有写权限,并且具备下面三个条件之一:
- 1、拥有该文件;2、拥有该目录;3、具有
root权限
- 1、拥有该文件;2、拥有该目录;3、具有
- 关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容
-
unlinkat
-
pathname
绝对路径,忽略fd
-
pathname
是相对路径,计算相对于fd
代表的目录的目录名,通常fd=AT_FDCWD
-
flag=AT_REMOVEDIR
,unlinkat函数
可以类似于rmdir
一样删除目录
-
#include
#include #include int main(){ if(open("tempfile",O_RDWR)<0) err_sys("open error"); if(unlink("tempfile")<0) err_sys("unlink error"); printf("file unlinked\n"); sleep(5); printf("done\n"); exit(0); } -
unlink
的关闭特性经常被用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用open
或creat
创建一个文件,然后立即调用unlink
,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(此时,内核关闭进程所打开的所有文件),该文件内容才被删除(上一章pub2
原子操作的实现好像可以借助unlink
啊) - 如果
pathname
是符号链接,那么unlink
删除该符号链接,而不是删除符号链接指向的文件;在给出符号链接的情况下,没有一个函数能删除由该符号链接所引用的文件 - 如果文件系统支持的话,超级用户能够使用
unlink
删除一个目录,但是通常应该使用rmdir
函数
- 这两个函数删除目录项,并将由
-
remove
函数解除对一个文件或目录的链接- 对于文件,
remove
与unlink
功能相同 - 对于目录,
remove
与rmdir
功能相同
- 对于文件,
函数rename和renameat
-
对文件或目录重命名
#include
int rename(const char *oldname, const char *newname); int renameat(int oldfd, const char *oldname, int newfd, const char *newname); -
如果
oldname
指向的是一个文件而不是目录,则为该文件或者符号链接重命名。- 如果
newname
存在,则它不能引用一个目录 - 如果
newname
存在,而且不是一个目录,则需要先删除该目录项(文件名)之后,才能正常重命名 - 调用进程必须在两个目录项中具有合适的写权限,因为需要更改目录项
- 如果
-
如果
oldname
指向一个目录,则为该目录重命名。- 如果
newname
已存在,则它必须引用一个目录,且是空目录(空目录下只包含.
和..
两项) - 如果
newname
已存在,而且是空目录,则先将该目录删除,然后为oldname
目录重命名 - 重命名时,
newname
不能包含oldname
作为其路径前缀(一个目录怎么能重命名为自己的子目录项呢?)
- 如果
如果
oldname
或newname
引用符号链接,则处理的是符号链接本身,而不是它们所指的文件不能对
.
和..
重命名如果
oldname
和newname
引用同一文件,函数不做任何更改而成功返回如果
newname
已经存在,则调用进程对它需要有写权限(如删除)。调用进程需要删除oldname
目录项,并可能创建newname
目录项,所以要对包含这两个目录项的目录具有写和执行权限除了当
newname
或oldname
指向相对路径时,其他情况下renameat
与rename函数
相同如果
oldname
指向相对路径,则根据oldfd
引用的目录来计算路径,newname
类似oldfd=AT_FDCWD
时,根据相对于当前目录来计算相应的路径名
符号链接
-
符号链接是对一个文件的间接指针。与硬链接不同,硬链接直接指向文件的
i节点
。- 硬链接通常要求链接和文件位于同一文件系统内
- 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)
符号链接一般用于将一个文件或整个目录结构移到系统的另一个位置
当使用以名字引用文件的函数时,应当注意该函数是否处理符号链接
-
实例:使用符号链接可能在系统中引入循环
$ mkdir foo $ touch foo/a $ ln -s ../foo foo/testdir $ ls -l foo
-
Solaris
的标准函数ftw(3)
以降序遍历文件结构,打印每个遇到的路径名,当处理foo
目录时,将会产生ELOOP
循环错误
消除循环:
unlink
并不跟随符号链接,可以unlink foo/testdir
,但是如果创建了一个构成循环的硬链接,则很难消除它
-
open函数
打开一个符号链接时,会跟随链接到达所指定的文件。如果符号链接指向的文件不存在,则出错返回
创建和读取符号链接
-
symlink
或symlinkat函数
创建一个符号链接#include
int symlink(const char *actualpath, const char *sympath); int symlinkat(cosnt char *actualpath, int fd, const char *sympath); - 创建符号链接时,不要求实际文件存在,且
actualpath
和sympath
不要求在同一个文件系统内 -
symlinkat
与symlink
类似,只不过可以根据相对路径计算
- 创建符号链接时,不要求实际文件存在,且
-
因为
open函数
跟随符号链接,所以需要一种方法打开该链接本身,并读该链接中的名字。readlink
和readlinkat
提供了这个功能#include
ssize_t readlink(const char *restrict pathname, const char *restrict buf, size_t bufsize); ssize_t readlinkat(int fd, const char *restrict pathname, const char *restrict buf, size_t bufsize);
文件的时间
-
每个文件属性所保存的实际精度依赖于文件系统的实现。
字段 说明 例子 ls(1)
选项st_atim
文件数据的最后访问时间 read
-u
st_mtim
文件数据的最后修改时间 write
默认 st_ctim
i节点
状态的最后修改时间chmod、chown
-c
函数futimens、utimensat和utimes
-
更改一个文件的访问和修改时间
#include
int futimens(int fd, const struct timespec times[2]); int utimensat(int fd, const char *path, const struct timespec times[2], int flag); - 这两个函数的
times
数组参数的第一个元素包含访问的时间,第二个元素包含修改时间。这两个时间是日历时间,是自特定时间(1970年1月1日 00:00:00)以来所经过的秒数,不足秒的部分用纳秒表示 - 时间戳的4种指定方式
-
-
times
参数是空指针,则设置为当前时间
-
-
times
参数指向两个timespec
结构的数据-
- 任一数组元素的
tv_nsec
字段的值为UTIME_NOW
,相应的时间戳就设置为当前时间,忽略相应的tv_sec
字段
- 任一数组元素的
-
- 任一数组元素的
tv_nsec
字段的值为UTIME_OMIT
,相应的时间戳就保持不变,忽略相应的tv_sec
字段
- 任一数组元素的
-
- 任一数组元素的
tv_nsec
字段的值不为上面两种,相应的时间戳设置为相应的tv_sec
和tv_nsec
字段的值
- 任一数组元素的
-
-
- 执行函数所要求的优先权取决于
times
参数的值- 如果
times
是一个空指针,或者任一tv_nsec
字段的值为UTIME_NOW
- 进程的
有效用户ID
必须等于文件的所有者ID
- 进程必须对该文件具有写权限,或者进程是一个超级用户进程
- 进程的
- 如果
times
是一个非空指针,且任一tv_nsec
字段既不是UTIME_NOW
,也不是UTIME_OMIT
- 进程的
有效用户ID
必须等于该文件的所有者ID
,或者进程必须是一个超级用户进程 - 只对文件具有写权限是不够的
- 进程的
- 如果
times
是一个非空指针,且两个tv_nsec
字段都为UTIME_OMIT
,就不执行任何权限检查(它什么都不会做,还检查什么?)
- 如果
-
futimens
需要打开文件来更改它的时间 -
utimensat
函数提供了一种使用文件名更改文件时间的方法 -
utimensat
函数的flag
指定修改符号链接本身的时间还是符号链接指向的文件的链接(使用AT_SYMLINK_NOFOLLOW
标志),默认跟随符号链接,并把文件的时间修改为符号链接的时间
- 这两个函数的
-
utimes函数
对路径名进行操作#include
int utimes(const char *pathname, const struct timeval times[2]); struct timeval{ time_t tv_sec; /* seconds */ long tv_usec; /* microseconds */ } - 我们不能对状态更改时间
st_ctim
(i节点
最近被修改的时间)指定一个值,因为调用utimes函数
时,此字段会被自动更新
- 我们不能对状态更改时间
```c
#include
#include
#include
int main(int argc, char *argv[]) {
int i, fd;
struct stat statbuf;
struct timespec times[2];
for (i = 1; i < argc; i++) {
if (stat(argv[i], &statbuf) < 0) {
err_ret("%s: stat error", argv[i]);
continue;
}
if ((fd = open(argv[i], O_RDWR | O_TRUNC)) < 0) {
err_ret("%s: open error", argv[i]);
continue;
}
//我的系统中stat返回的time_t结构,根据需要调整
times[0].tv_sec = statbuf.st_atime;
times[1].tv_sec = statbuf.st_mtime;
if (futimens(fd, times) < 0) {
err_ret("%s: futimens error", argv[i]);
}
close(fd);
}
}
```
函数mkdir、mkdirat和rmdir
-
mkdir
、mkdirat
创建目录,用rmdir
删除目录#include
int mkdir(const char *pathname, mode_t mode); int mkdirat(int fd, const char *pathname, mode_t mode); #include int rmdir(const char *pathname); 对于目录,至少要设置一个执行权限位,以允许访问该目录中的文件名
读目录
对某个目录具有访问权限的任一用户都可以读该目录,但是,为了防止文件系统产生混乱,只有内核才能写目录
一个目录的写权限位以及执行权限位决定了在该目录中能否创建新文件以及删除文件,但它们并不表示能否写目录本身
-
目录的实际格式与
UNIX
系统实现和文件系统的设计有关- 早期的系统(如
V7
):每个目录项16字节,14字节是文件名(固定长度),2字节是i节点编号 -
4.2BSD
:允许更长的文件名,每个目录项长度可变 - 很多实现阻止应用程序使用
read函数
读取目录内容
#include
DIR *opendir(const char *pathname); DIR *fdopendir(int fd); struct dirent *readdir(DIR *dp); void rewinddir(DIR *dp); int closedir(DIR *dp); long telldir(DIR *dp); void seekdir(DIR *dp,long loc); - 早期的系统(如
fdopendir
函数最早出现在SUSv4
中,它可以把打开文件描述符转换成目录处理函数需要的DIR结构
telldir
和seekdir函数
不是基本POSIX.1
标准的组成部分,是SUS
中的XSI扩展
,所以符合UNIX
系统的实现都会提供这两个函数-
定义在
头文件中的dirent
结构与实现有关。实现对此结构所做的定义至少包含下列两个成员ino_t d_ino; /* i-node number */ char d_name[]; /* null-terminated filename */
-
实例:遍历文件层次结构
#include
#include #include #include #include typedef int MyFunc(const char *, const struct stat *, int); static MyFunc myfunc; static int myftw(char *, MyFunc *); static int dopath(MyFunc *); static long nreg, ndir, nblk, nchr, nfifo, nslink, nsock, ntot; int main(int argc, char *argv[]) { int ret; if (argc != 2) err_quit("usage: ftw "); ret = myftw(argv[1], myfunc); ntot = nreg + ndir + nblk + nchr + nfifo + nslink + nsock; if (ntot == 0) ntot = 1; printf("regular files = %7ld, %5.2f %%\n", nreg, nreg * 100.0 / ntot); printf("directories = %7ld, %5.2f %%\n", ndir, ndir * 100.0 / ntot); printf("block special = %7ld, %5.2f %%\n", nblk, nblk * 100.0 / ntot); printf("char special = %7ld, %5.2f %%\n", nchr, nchr * 100.0 / ntot); printf("FIFOs = %7ld, %5.2f %%\n", nfifo, nfifo * 100.0 / ntot); printf("symbolic links = %7ld, %5.2f %%\n", nslink, nslink * 100.0 / ntot); printf("sockets = %7ld, %5.2f %%\n", nsock, nsock * 100.0 / ntot); exit(ret); } #define FTW_F 1 #define FTW_D 2 #define FTW_DNR 3 #define FTW_NS 4 static char *fullpath; static size_t pathlen; static int myftw(char *pathname, MyFunc *func) { fullpath = path_alloc(&pathlen);/* malloc PATH_MAX+1 bytes */ if (pathlen <= strlen(pathname)) { pathlen = strlen(pathname) * 2; if ((fullpath = realloc(fullpath, pathlen)) == NULL) err_sys("realloc failed"); } strcpy(fullpath, pathname); return dopath(func); } static int dopath(MyFunc *func) { struct stat statbuf; struct dirent *dirp; DIR *dp; int ret, n; if (lstat(fullpath, &statbuf) < 0) return func(fullpath, &statbuf, FTW_NS); if (S_ISDIR(statbuf.st_mode) == 0) return func(fullpath, &statbuf, FTW_F); if ((ret = func(fullpath, &statbuf, FTW_D)) != 0) return ret; n = strlen(fullpath); if (n + NAME_MAX + 2 > pathlen) { pathlen *= 2; if ((fullpath = realloc(fullpath, pathlen)) == NULL) err_sys("realloc failed"); } fullpath[n++] = '/'; fullpath[n] = 0; if ((dp = opendir(fullpath)) == NULL) return func(fullpath, &statbuf, FTW_DNR); while ((dirp = readdir(dp)) != NULL) { if (strcmp(dirp->d_name, ".") == 0 || strcmp(dirp->d_name, "..") == 0) continue; strcpy(&fullpath[n], dirp->d_name); if ((ret = dopath(func) != 0)) break; } fullpath[n - 1] = 0; if (closedir(dp) < 0) err_ret("can't close directory %s", fullpath); return ret; } static int myfunc(const char *pathname, const struct stat *statptr, int type) { switch (type) { case FTW_F: switch (statptr->st_mode & S_IFMT) { case S_IFREG: nreg++; break; case S_IFBLK: nblk++; break; case S_IFCHR: nchr++; break; case S_IFIFO: nfifo++; break; case S_IFSOCK: nsock++; break; case S_IFDIR: err_dump("for S_IFDIR for %s", pathname); } break; case FTW_D: ndir++; break; case FTW_DNR: err_ret("can't read directory %s", pathname); break; case FTW_NS: err_ret("stat error for %s", pathname); break; default: err_dump("unknown type %d for pathname %s", type, pathname); } return 0; } regular files = 132, 85.16 % directories = 23, 14.84 % block special = 0, 0.00 % char special = 0, 0.00 % FIFOs = 0, 0.00 % symbolic links = 0, 0.00 % sockets = 0, 0.00 %
- pathalloc.h:实际上应该编写
cmake
脚本或者makefile
以动态链接库的形式使用pathalloc函数
,这里为了方便直接使用头文件的方式 - pathalloc.h代码
- pathalloc.h:实际上应该编写
函数chdir、fchdir和getcwd
每个进程都有一个工作目录,此路径是搜索所有相对路径名的起点
当用户登录到
UNIX
系统时,其当前工作目录通常是口令文件(etc/passwd)
中该用户登录项的第6个字段——用户的起始目录当前工作目录是进程的一个属性
起始目录是登录名的一个属性
-
进程调用
chdir
或fchdir函数
可以更改当前工作目录#include
int chdir(const char *pathname); int fchdir(int fd); char *getcwd(char *buf, size_t size); #include
#include int main(){ if(chdir("/tmp")<0) err_sys("chdir failed"); printf("chdir to /tmp succeeded\n"); exit(0); } -
chdir
只能改变进程的工作目录,shell
是一个单独的进程,所以为了改变shell
进程自己的工作目录,shell
应当直接调用chdir
函数,为此,cd命令
内建在shell
中
-
-
因为内核必须维护当前工作目录的信息,所以应当能获取其当前值。但是,内核为每个进程只保存指向该目录
v节点
的指针等目录本身的信息,并不保存该目录的完整路径名-
Linux内核
可以确定完整路径名。完整路径名的各个组成部分分布在mount表
和dcache表
中,然后进行重新组装,比如在读取/proc/self/cwd
符号链接时
-
我们需要一个函数,它从当前工作目录的(
.
)开始,用..
找到其上一级目录,然后读取其目录项,直到该目录项中的i节点
编号与工作目录i节点
编号相同,这样就找到了其对应的文件名,按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。getcwd
提供了这种功能。-
getcwd
需要一个缓冲区地址buf
存储绝对路径名,还有一个缓冲区大小参数,缓冲区大小必须具有足够的长度以容纳绝对路径名再加上一个终止null
字节,否则返回出错#include
#include #include int main() { char *ptr; size_t size; if (chdir("..") < 0) err_sys("chdir failed"); ptr = path_alloc(&size); if (getcwd(ptr, size) == NULL) err_sys("getcwd failed"); printf("cwd = %s\n", ptr); exit(0); } -
chdir
跟随符号链接 -
fchdir
:先用open
打开当前工作目录,保存起返回的文件描述符。当希望回到原工作空间时,只需要简单的将该文件描述符传送给fchdir
-
设备特殊文件
-
st_dev
和st_rdev
- 每个文件系统所在的存储设备都由其主、次设备号表示。
- 设备号所用的数据类型是基本系统数据类型
dev_t
- 主设备号标识设备驱动程序,有时编码为与其通讯的外设板
- 次设备号标识特定的子设备
- 设备号所用的数据类型是基本系统数据类型
- 通常可以使用两个宏:
major
和minor
来访问主、次设备号,大多数实现都定义这两个宏 - 系统中与每个文件名关联的
st_dev
值是文件系统的设备号,该文件系统包含了这一文件名以及与其对应的i节点
- 只有
字符特殊文件
和块特殊文件
才有st_rdev
值。此值包含实际设备的设备号
#include
#include #include //莫名其妙的不起作用,直接将major、 minor拷贝过来 #define major(x) ((int32_t)(((u_int32_t)(x) >> 24) & 0xff)) #define minor(x) ((int32_t)((x) & 0xffffff)) int main(int argc, char *argv[]) { int i; struct stat buf; for (i = 1; i < argc; i++) { printf("%s :", argv[i]); if (stat(argv[i], &buf) < 0) { err_ret("stat error"); continue; } printf("dev = %d/%d", major(buf.st_dev), minor(buf.st_dev)); if (S_ISCHR(buf.st_mode) || S_ISBLK(buf.st_mode)) { printf(" (%s) rdev = %d/%d", (S_ISCHR(buf.st_mode)) ? "character" : "block", major(buf.st_rdev), minor(buf.st_rdev)); } printf("\n"); } exit(0); } - 每个文件系统所在的存储设备都由其主、次设备号表示。
文件访问权限位小结
习题
-
stat
替换lstat
函数-
stat
总是跟随符号链接,不会显示文件类型是符号链接 - 访问不存在的文件会出错
-
-
umask 777
- 屏蔽文件所有访问权限,就如
chmod 777
打开文件所有权限
- 屏蔽文件所有访问权限,就如
用户关闭自己拥有文件的用户读权限之后,将不能访问自己的文件
-
用
open
、creat
创建已经存在的文件,将会怎么样?- 已存在的文件访问权限不变
- 但文件被截断
-
目录和符号链接的长度
st_size
是否可以为0- 普通文件的长度可以为0
- 目录至少包含
.
和..
两项,长度不为0 - 符号链接的长度是其路径名包含的字符数,路径长度至少为1,长度也不为0
-
编写一个类似
cp(1)
的程序,它复制包含空洞的文件,但不将字节0写到输出文件中去。
原文链接#include
#include #include #include #include #include #define BUF_SIZE 4096 int my_cp(const char *file1, const char *file2) { int fd1, fd2; char buffer[BUF_SIZE]; int res, current_position = 0, byte_count =0, have_holes = 0; struct stat st; fd1 = open(file1, O_RDWR); if(-1 == fd1){ perror("open file1 faild"); return -1; } if(fstat ( fd1, &st) !=0) perror("fstat"); else{ if (S_ISREG(st.st_mode) && st.st_size > 512 * st.st_blocks) { have_holes = 1; printf("%s is a sparse-block file!\n", file1); } else{ have_holes = 0; printf("%s is not a sparse-block file!\n", file1); } } fd2 = open(file2, O_RDWR | O_APPEND | O_CREAT | O_TRUNC, 0666); if ( -1 == fd2) { perror ("open file2 faild"); return -1; } memset(buffer, '\0', BUF_SIZE); res = read(fd1, buffer, BUF_SIZE);//返回读到的字节数 if ( -1 == res) { perror ("file1 read error"); return -1; } if(have_holes){ byte_count =0; for (current_position = 0; current_position < res; current_position ++) { if (0 != buffer[current_position] ) { buffer[byte_count] = buffer[current_position]; byte_count ++; } } }else byte_count = res; res = write(fd2, buffer, byte_count); if ( -1 == res){ perror( " file2 write error"); return -1; } close(fd1); close(fd2); } int main(int argc, char * argv[]) { if (3 != argc){ printf("usage error : ./a.out file1 file2\n"); return -1; } int res = my_cp(argv[1], argv[2]); if ( -1 == res) { perror (" my_cp error"); return -1; } exit(0); } -
在没有更改
umask
的前提下,内核拷贝的文件权限与原文件权限不一致,为什么?- 它们属于不同的进程,拥有的
umask
不是同一个
- 它们属于不同的进程,拥有的
-
df(1)
命令和du(1)
命令检查空闲的磁盘空间的区别-
du命令
检查需要文件名或目录名,当unlink
没有返回时,文件名没有了,但是内容还没有清除,du命令
不会计算这部分内存 - 此时只能使用df命令查看文件系统中实际可用的空闲空间
-
-
unlink函数
会修改文件状态更改时间,这是怎样发生的?- 如果删除的链接不是最后一个链接,此时修改文件状态更改时间
- 如果删除的是最后一个链接,此时文件将被物理删除,此时修改文件状态更改时间没有意义
-
系统对可打开文件数的限制对
myftw函数
有什么影响?- 每次降一级就要使用另一个文件描述符,所以进程可以打开的文件数限制了我们可以遍历的文件系统树的深度。
-
SUS
的XSI
扩展中说明ftw
允许调用者指定使用的描述符数,这隐含着可以关闭描述符并且重用它们
myftw
函数从不改变其目录,如果每次访问下一个遇到的目录,使用chdir
修改当前工作目录到遇到的目录,这样可以使用文件名而非路径调用lstat
,访问结束后在利用chdir("..")
修改回工作目录,这样会更快吗?每个进程都有一个根目录用于解析绝对路径名,可以通过
chroot函数
改变根目录。这个函数什么时候用?如何只设置两个时间值中的一个来使用
utimes函数
?略
略
可以看一看
略