本文是笔者拜读《UNIX环境高级编程》第4章(文件和目录)的学习笔记。本文的主要内容包括link、rename、符号链接、文件的时间、读目录和一个统计文件类型的实例。文中不仅包含书中的知识点,也包括笔者的理解。
目录
link、linkat、unlink、unlinkat和remove
可以有多个目录项指向任一文件的i
节点。使用link
和linkat
可以创建一个指向现有文件的链接。
成功返回0
,失败返回-1
.
这两个函数创建了一个新目录项newpath
,它引用现有文件oldpath
。如果newoath
已存在则报错。只创建newpath
中的最后一个分量,路径中的其他部分应当已经存在。
对于linkat
,引用的现有文件是通过olddirfd
和oldpath
确定的,新的路径名是通过newdirfd
和newpath
确定的。olddirfd
和newdirfd
是某个打开目录的文件描述符。
当现有文件是符号链接时,如果flag
中参数设置了AT_SYMLINK_FOLLOW
,就创建指向符号链接所引用的文件的链接,否则创建指向符号链接本身的链接。
创建新目录项和增加链接计数是一个原子操作。
大多数的链接实现,要求现有的和新建的文件在同一文件系统中。
只有超级用户才能创建一个指向目录的硬链接,因为这样可能会形成循环。很多文件系统实现不允许对目录的硬链接。
使用unlink
能删除现有的目录项。
成功返回0
,失败返回-1
.
这两个函数删除目录项,并将pathname
所引用的文件链接计数减1
。如果函数调用失败,则不对文件做任何更改。
为了解除对文件的链接,必须对包含该目录项的目录具有写和执行权限。
只有当链接计数为0
时,该文件的内容才被删除。但只要有进程打开了该文件,也不能删除其内容。关闭一个文件时,内核首先检查打开该文件的进程个数,如果是0
,内核再去检查其链接计数,如果也是0
,才删除该文件。
如果unlinkat
函数的flag
参数被设置为AT_REMOVEDIR
,该函数可以类似于rmdir
删除目录。
进程用open
或creat
创建一个文件,然后立即调用unlink
,因为有进程打开了该文件,所以并不会立即删除该文件。只有当进程关闭该文件或进程终止时,文件被删除。
如果pathname
是符号链接,那么unlink
删除符号链接,而不是它所引用的文件。
也可以用remove
函数解除对一个文件或目录的链接。对于文件remove == unlink
,对于目录remove == rmdir
rename、renameat
文件或目录可以用rename
或renameat
进行重命名。
成功返回0
,失败返回-1
·
根据oldpath
是文件、目录还是符号链接,有以下情况:
(1)
如果oldpath
指的是文件,而newpath
已存在(不是目录),则先将该目录项删除,然后将oldpath
重命名为newpath
。
(2)
如果oldpath
指的是目录,而newpath
已存在(是空目录),则将其删除,然后将oldpath
重命名为newpath
。newpath
不能包含oldpath
作为其路径前缀。
(3)
如果oldpath
和newpath
引用符号链接,则处理的是符号链接本身。
(4)
不能对.
和..
重命名。
(5)
如果oldpath
等于newpath
,函数不做任何更改并成功返回。
调用进程会删除oldpath
目录项,创建newpath
目录项,需要对包含两者的目录有写和执行权限。
符号链接
符号链接是对一个文件的间接指针,而硬链接直接指向了文件的i
节点。
对符号链接以及它指向何种对象并无任何文件系统限制,任何用户都可以创建指向目录的符号链接。
当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接。如果路径名是符号链接,mkdir
、mkinfo
、mknod
和rmdir
这些函数会出错返回。
函数 | 是否跟随符号链接 |
---|---|
access | 是 |
chdir | 是 |
chmod | 是 |
chown | 是 |
creat | 是 |
exec | 是 |
lchown | 否 |
link | 是 |
lstat | 否 |
open | 是 |
opendir | 是 |
pathconf | 是 |
readlink | 否 |
remove | 否 |
rename | 否 |
stat | 是 |
truncate | 是 |
unlink | 否 |
读取和创建符号链接
函数创建了一个指向target
的新目录项linkpath
。
target
可以不存在,target
和linkpath
可以不在同一文件系统中。
使用readlink
可以打开链接本身,并读该链接中的名字。
函数执行成功,返回读入buf
的字节数,在buf
中返回的符号链接的内容不以null
终止。失败返回-1
.
文件的时间
在stat
结构中,每个文件维护3
个时间字段。
字段 | 说明 | 例子 | ls选项 |
---|---|---|---|
st_atim | 文件数据的最后访问时间 | read | -u |
st_mtim | 文件数据的最后修改时间 | write | 默认 |
st_ctim | i 节点状态的最后更改时间 | chmod、chown | -c |
修改时间是文件内容最后一次被修改的时间,状态更改时间是该文件的i
节点最后一次被修改的时间。更改文件的访问权限、用户ID、链接数等会影响i
节点。系统不维护对i
节点的最后一次访问。
futimens、utimensat和utimes
一个文件的访问和修改时间可以用以下几个函数更改。futimens
和utimensat
可以指定纳秒级精度的时间戳。
这两个函数的times
数组参数的第一个元素是访问时间,第二个是修改时间。用秒和纳秒表示时间。这是自特定时间(1970年1月1日00:00:00
)以来所经过的秒数,不足一秒的部分用纳秒表示。
utimes
函数对路径名进行操作。
mkdir、mkdirat和rmdir
mkdir
和mkdirat
用于创建目录。成功返回0
,失败返回-1
.
对目录指定的权限要大一些,至少要设置执行权限位,允许访问该目录中的文件名。
用rmdir
函数可以删除一个空目录(只包含./
和../
)。
读目录
成功返回指针,失败返回NULL
.
fdopendir
可以把打开文件描述符转换成目录处理函数需要的DIR
结构。
成功返回指针,失败或在目录尾返回NULL
。
dirent
结构至少包含以下两个成员:
ino_t d_ino; // i节点编号
char d_name[]; // 文件名,以'\0'为结尾
目录中各目录项的顺序与实现有关,它们通常并不按字母顺序排列。
看得让人头大。别试图理解书中所有的东西,有个大概印象即可。
主要靠项目实践和做题巩固知识。
感觉书中有些地方翻译得不太行。
实例
输入是起点路径名,从该点开始递归降序遍历文件层次结构,最终得到各类型的文件计数。
先简单描述一下思路:
/* 定义所需的全局变量,统计各类型的文件数量 */
/* 根据非目录文件的类型更新计数值 */
void FileCnt();
/* 以pathname作为起点路径名,统计数据 */
int FileTree(const char *pathname) {
if (/* 获取文件状态失败 */) {
return -1;
}
int ret = 0;
if (/* pathname是个目录 */) {
// 更新目录文件计数
while (/*在pathname中能读到目录项*/) {
ret = FileTree(/* 目录项 */);
}
}
else {
// 更新相应的文件计数
FileCnt();
}
return ret;
}
//
int main(int argc, char **argv) {
FileTree(argv[1]);
/* 打印统计结果 */
return 0;
}
具体实现:
// count.c
#include <stdio.h>
#include <dirent.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
int nTtl; // 文件总数
int nReg; // 普通文件个数
int nDir; // 目录文件个数
int nFifo; // 命名管道个数
int nChr; // 字符特殊文件个数
int nBlk; // 块特殊文件个数
int nSlink; // 符号链接文件个数
int nSock; // 套接字个数
struct stat statBuf;
void FileCnt() {
if (S_ISREG(statBuf.st_mode)) {
++nReg;
}
else if (S_ISCHR(statBuf.st_mode)) {
++nChr;
}
else if (S_ISBLK(statBuf.st_mode)) {
++nBlk;
}
else if (S_ISFIFO(statBuf.st_mode)) {
++nFifo;
}
else if (S_ISLNK(statBuf.st_mode)) {
++nSlink;
}
else if (S_ISSOCK(statBuf.st_mode)) {
++nSock;
}
}
int FileTree(const char *path) {
int ret = 0;
if (lstat(path, &statBuf) < 0) {
fprintf(stderr, "%s: ", path);
perror("lstat error");
return -1;
}
if (S_ISDIR(statBuf.st_mode)) {
++nDir;
DIR *dir = opendir(path);
struct dirent *psd;
while (psd = readdir(dir)) {
if (strcmp(psd->d_name, ".") == 0 ||
strcmp(psd->d_name, "..") == 0) {
continue;
}
char str[300];
sprintf(str, "%s/%s", path, psd->d_name);
ret = FileTree(str);
}
}
else {
FileCnt();
}
return ret;
}
int main(int argc, char **argv) {
if (argc != 2) {
printf("invalid input\n");
return -1;
}
int ret = FileTree(argv[1]);
nTtl = nReg + nDir + nChr + nBlk + nSlink + nFifo + nSock;
nTtl = (nTtl == 0) ? 1 : nTtl;
printf("普通文件数量:%2d, 占比:%4.2f%%\n", nReg, nReg * 100.0 / nTtl);
printf("目录文件数量:%2d, 占比:%4.2f%%\n", nDir, nDir * 100.0 / nTtl);
printf("字符特殊文件数量:%2d, 占比:%4.2f%%\n", nChr, nChr * 100.0 / nTtl);
printf("块特殊文件数量:%2d, 占比:%4.2f%%\n", nBlk, nBlk * 100.0 / nTtl);
printf("命名管道文件数量:%2d, 占比:%4.2f%%\n", nFifo, nFifo * 100.0 / nTtl);
printf("符号链接文件数量:%2d, 占比:%4.2f%%\n", nSlink, nSlink * 100.0 / nTtl);
printf("套接字文件数量:%2d, 占比:%4.2f%%\n", nSock, nSock * 100.0 / nTtl);
return 0;
}
运行结果如下:
该程序是笔者根据自己的理解写的,虽然不如书上的实例通用,但大概能实现期望的功能(文件数量过多时,程序会出现段错误)。
该程序所统计的目录文件数比期望的多1
,因为程序把main
函数的参数argv[1]
也算作了一个目录。