Unix编程学习笔记-文件和目录

风尘

文章目录

  1. 1. 函数stat
  2. 2. 文件类型
  3. 3. 函数access和faccessat
  4. 4. 文件权限
    1. 4.1. 函数umask
    2. 4.2. 函数chmod
    3. 4.3. 函数chown
  5. 5. 文件长度与截断
  6. 6. 函数link、unlink
  7. 7. 函数rename和renameat
  8. 8. 符号链接
    1. 8.1. 创建符号链接
    2. 8.2. 读取符号链接
  9. 9. 文件的时间
  10. 10. 函数futimens、utimensat和utimes
  11. 11. 函数mkdir、mkdirat和rmdir
  12. 12. 读文件
  13. 13. 函数chdir、fchdir和getcwd

函数stat

1
2
3
4
5
6
7
#include <sys/stat.h>

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);
// 这四个函数返回值:成功返回0;出错返回-1

(1) 当给出 pathname 时:

  • stat 函数将返回与此命名文件有关的信息结构。
  • fstat 函数获得已在描述符 fd 上打开文件的有关信息。
  • lstat 函数类似于 stat,但当命名文件是一个符号链接时,lstat 返回该符号链接的有关信息,而不是由该符号链接引用的文件信息。此外,当以降序遍历目录层次结构时,需要用到此函数。
  • fstatat 函数为一个相对于当前打开目录(由 fd 参数指向)的路径名返回文件统计信息。
    • flag 参数表示是否跟随一个符号链接。设置AT_SYMLINK_NOFOLLOW标志表示不会跟随符号链接,而是返回符号本身的信息。否则默认返回符号链接所指向的实际文件的信息。
    • fd 参数值为AT_FDCWD,并且 pathname 参数是一个相对路径名,函数将会计算相对于当前目录的 pathname 参数;如果 pathname 是一个绝对路径,fd 参数就会被忽略。这两种情况,根据 flag 的取值,函数作用跟 stat 或 lstat 一样。

(2) buf 参数是一个指向struct stat结构的指针。结构的实际定义可能根据气体实现有所不同,但基本形式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct stat {
dev_t st_dev; /* ID of device containing file -文件所在设备的ID*/
ino_t st_ino; /* inode number -inode节点号*/
mode_t st_mode; /* protection -保护模式?*/
nlink_t st_nlink; /* number of hard links -链向此文件的连接数(硬连接)*/
uid_t st_uid; /* user ID of owner -user id*/
gid_t st_gid; /* group ID of owner - group id*/
dev_t st_rdev; /* device ID (if special file) -设备号,针对设备文件*/
off_t st_size; /* total size, in bytes -文件大小,字节为单位*/
blksize_t st_blksize; /* blocksize for filesystem I/O -系统块的大小*/
blkcnt_t st_blocks; /* number of blocks allocated -文件所占块数*/
time_t st_atime; /* time of last access -最近存取时间*/
time_t st_mtime; /* time of last modification -最近修改时间*/
time_t st_ctime; /* time of last status change - */
}

使用 stat 函数最多的地方可能就是ls -l命令,用其可以获得有关文件的所有信息。

文件类型

Unix 的文件类型主要包括下列几种:

  • 普通文件( regular file )最常用的文件类型,包含了某种形式的数据,数据或是二进制数据,但对于 UNIX 内核来说并无区别。对普通文件内容的解释由处理该文件的应用程序进行。

    值得注意的是二进制可执行文件。为了执行程序,内核必须理解其格式。所有二进制可执行文件都遵循一种标准化的格式,通过这种格式内核能够确定程序文本和数据的加载位置。

  • 目录文件( directory file )包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任一进程都可以读该目录的内容,但只有内核可以直接写目录文件。进程必须通过系统函数才能更改目录。

  • 块特殊文件( block special file )提供对设备(如磁盘)带缓冲的访问,每次访问以固定长度为单位进行。

    FreeBSD 不再支持块特殊文件。对设置的所有访问需要通过字符特殊文件进行。

  • 字符特殊文件( character special file )提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是志特殊文件。

  • FIFO用于进程间通信,有时也称为命名管道( named pipe )

  • 套接字( socket )用于进程间的网络通信。也可用于在一台宿主机上进程之间的非网络通信。

  • 符号链接( symbolic link )指向另一个文件。

文件类型信息包含在 stat 结构的 st_mode 成员中,可以使用定义的宏确定文件类型,宏参数都是 stat 结构中的 st_mode 成员。

文件类型
S_ISREG() 普通文件
S_ISDIR() 目录文件
S_ISCHR() 字符特殊文件
S_ISBLK() 块特殊文件
S_ISFIFO() 管道或FIFO
S_ISLNK() 符号链接
S_ISSOCK() 套接字
实例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include "common/apue.h"
#include <errno.h>

int main() {
struct stat buf;

int r = stat("/Users/windus/iwork/code/c_c++/algorithms_in_c/unix_c.c", &buf);

if (r != -1) {
if (S_ISREG(buf.st_mode))
printf("普通文件类型");

} else {
printf("程序出错,编号:%d", errno);
}
}

POSIX.1 允许实现将进程间通信(IPC)对象(如消息队列和信号量等)说明为文件。可以通过下列宏确定 IPC 对象的类型,它们的参数是指向 stat 结构的指针,而并非 st_mode。

对象的类型
S_TYPEISMQ() 消息队列
S_TYPEISSEM() 信号量
S_TYPEISSHM() 共享存储对象

函数accessfaccessat

这两个是按实际用户ID和实际组ID进行访问权限测试的函数。

1
2
3
4
5
#include <unistd.h>

int access(const char *pathname, int mode);
int faccessat(int fd, const char *pathname, int mode, int flag);
// 两个函数的返回值:成功返回0; 失败返回-1

mode参数可选值:

  • F_OK测试文件存在
  • R_OK测试读权限
  • W_OK测试写权限
  • X_OK测试执行权限

faccessat 函数与 access 函数在下面两种情况下是相同的:

  1. pathname 参数为绝对路径
  2. fd 参数取值为 AT_FDCWD 而 pathname 参数为相对路径,否则 faccessat 计算相对于打开目录(fd 参数指向)的 pathname。

flag参数,设置为 AT_EACCESS,访问检查调用进程的有效用户 ID 和有效组 ID,而不是实际用户 ID 和实际组 ID。

文件权限

所有文件类型都有访问权限,st_mode 值也包含了对文件的访问权限位。每个文件有9个访问权限位,可将它们分类3类:

  • S_IRUSR 用户读
  • S_IWUSR 用户写
  • S_IXUSR 用户执行

  • S_IRGRP 组读
  • S_IWGRP 组写
  • S_IXGRP 组执行

  • S_IROTH 其他读
  • S_IWOTH 其他写
  • S_IXOTH 其他执行

函数umask

函数为进程设置文件模式屏蔽字,并返回之前的值。即在创建新文件或目录时屏蔽掉希望新文件或目录不应有的访问权限。

1
2
#include <sys/stat.h> // 返回值:设置前的文件创建权限
mode_t umask(mode_t cmask);

cmask参数由 9 个访问权限位常量(S_IRUSR、S_IWUSR等)中的若干个按位“或”(|)构成。
在进程创建一个新文件或目录时,就一定会使用文件模式创建屏蔽字。

示例代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int main() {
// 0表示不设置任何屏蔽字,即使用默认屏蔽字
umask(0);
if (creat("/Users/windus/iwork/code/c_c++/algorithms_in_c/tttttttt.d", 0777) < 0) {
printf("创建d文件失败");
}

// 设置用户读、用户写屏蔽字
umask(S_IRUSR | S_IWUSR);
if (creat("/Users/windus/iwork/code/c_c++/algorithms_in_c/tttttttt.r", 0777) < 0) {
printf("创建r文件失败");
}
}

// 在bash中查看文件权限
$ ll
-rwxrwxrwx 1 windus staff 0B 3 3 13:54 tttttttt.d
---xrwxrwx 1 windus staff 0B 3 3 13:54 tttttttt.r

UNIX 所有 shell 都有内置命令umask设置默认屏蔽字,更多关于 umask 命令1

函数chmod

这3个函数可以更改现有文件访问权限。

1
2
3
4
5
6
#include <sys/stat.h>

int chmod(const char *pathname, mode_t mode);
int fchmod(int fd, mode_t mode);
int fchmodat(int fd, const char *pathname, mode_t mode, int flag);
// 返回值:成功返回0; 失败返回-1

chmod 函数在对定文件上进行操作。
fchmod 函数在对已打开文件进行操作。

fchmodat 函数与 chmod 函数作用相同的两种情况:

  1. pathname 参数为绝对路径。
  2. fd 参数取值为 AT_FDCWD 而 pathname 参数为相对路径。

否则,fchmodat 函数就是对于打开目录(fd 参数指向)的 pathname 进行操作。
flag参数设置为 AT_SYMLINK_NOFOLLOW 时,fchmodat 并不会跟随符号链接。
mode参数是除了文件的9个权限访问位常量外,还有下面6个常量的按位或组合:

  • S_IRWXU 用户(所有者)读、写和执行
  • S_IRWXG 组读、写和执行
  • S_IRWXO 其他读、写和执行
  • S_ISUID 文件特殊权限 SUID 权限
  • S_ISGID 文件特殊权限 SGID 权限
  • S_ISVTX 文件特殊权限 SBIT 权限(粘着位)

更多关于文件特殊权限2

为了改变一个文件的权限位,进程的有效用户ID必须等于文件的所有者ID,或者该进程必须具有超级用户权限。

函数chown

1
2
3
4
5
6
7
#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 group, int flag);
int lchown(const char *pathname, uid_t owner, gid_t group);
// 返回值:成功返回0; 失败返回-1

当操作的文件不是符号链接时,这4个函数作用类似。
当操作文件是符号链接时,lchown 和 fchownat(设置了 AT_SYMLINK_NOFOLLOW 标志)更改符号链接本身的所有者,而不是该符号链接所指向的文件所有者。
fchown 函数操作已打开(fd 参数指向)文件的所有者。既然它在一个已打开的文件上操作,就不能用于改变符号链接的所有者。

fchownat、chown、lchown作用相同的两种情况:

  1. pathname 参数为绝对路径。
  2. fd 参数取值为 AT_FDCWD 而 pathname 参数为相对路径。

如果 flag 参数清除了 AT_SYMLINK_NOFOLLOW 标志(即设置为 AT_SYMLINK_FOLLOW ),则 fchownat 与 chown 作用相同。
如果 fd 参数设置为打开目录的文件描述符,并且 pathname 参数是一个相对路径名,fchownat 函数将作用于打开的目录。

基于 BSD 的系统规定只有超级用户才能更改一个文件的所有者,原因是防止用户改变其文件的所有者从而摆脱磁盘空间限额对他们的限制。System V 则允许任一用户更改他们所拥有的文件的所有者。
按照 POSIX.1 允许通过设置 _POSIX_CHOWN_RESTRICTED 的值在这两种形式的操作中选用一种。
对于 SOLARIS 10 HX 此功能是个配置选项,其默认值是施加限制。而 FreeBSD 8.0、Linux 3.2.0和Mac OS X 10.6.8则总对 chown 施加限制。

若 _POSIX_CHOWN_RESTRICTED 对指定文件生效,则:

  1. 只有超级用户进程能更改该文件的用户ID。
  2. 如果进程拥有此文件(其有效用户ID等于该文件的用户ID),参数 ower 等于 -1 或文件的用户ID,并且参数 group 等于进程的有效组 ID 或进程的附属组 ID 之王,那么非超级用户进程可以一颗心该文件的组ID。

这意味着,当 _POSIX_CHOWN_RESTRICTED 有效时,不能更改其他用户文件的用户ID,可以一颗心你所拥有的文件的组 ID,但只能改到你所属的组。

文件长度与截断

stat 结构成员 st_size 表示以字节为单位的文件长度,此字符只对普通文件、目录文件和符号链接有意义。

FreeBSD、Mac OS X 10.6.8和 Solaris 10对管道也定义了文件长度,它表示可从该管道中读到的字节数。

对于符号链接,文件长度是在文件名中的实际字节数,如下面例子,文件长度 10 就是路径名private/tmp的长度:

1
2
$ ll /
lrwxr-xr-x@ 1 root admin 11B 10 30 17:00 tmp -> private/tmp

大多数现代 UNIX 系统提供字段 st_blksize 和 st_blocks。其中,第一个是文件 I/O 块长度,第二个是所分配的实际块数。

为了截断文件可以调用函数truncateftruncate:

1
2
3
4
5
#include <unistd.h>

int truncate(const char *pathname, off_t length);
int ftruncate(int fd, off_t length);
// 返回值:成功返回0; 失败返回-1

这两个函数将一个现有文件长度截断为 lenth。如果该文件长度大于 length,则超过部分不能再访问;如果长度小于 length,文件长度将增加,其数据读作0(也就是可能在文件中创建了一个空洞)。

创建一个指向现有文件的硬链接的方法是使用函数 link 或 linkat 函数:

1
2
3
4
5
#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);
// 返回值:成功返回0; 失败返回-1

这两个函数创建一个新目录项 newpath,它引用现有文件 existingpath。如果 newpath 已经存在,则返回出错。函数只创建 newpath 中的最后一个分量,路径中的其他部分应当已经存在。

linkat 函数,现有文件是通过 efd 和existingpath 参数指定的,新的路径名是通过 nfd 和 newpath 参数指定。默认情况下,如果两个路径名中的任一个是相对路径,两个文件描述符中的任一个设置为 AT_FDCWD,相应的路径名就通过相对于当前目录进行计算。如果任一路径名是绝对路径,相应的文件描述符参数就会被忽略。

当现有文件是符号链接时,flag 参数设置 AT_SYMLINK_FOLLOW 标志,则创建指向符号链接目标的链接;反之,则创建一个指向符号链接本身的链接。
更多关于文件链接3

创建的链接可以通过 unlink 函数删除:

1
2
3
4
5
#include <unistd.h>

int unlink(const char *pathname);
int unlinkat(int fd, const char *pathname, int flag);
// 返回值:成功返回0; 失败返回-1

如果 pathname 参数是相对路径名,fd 文件描述符参数代表的目录的路径名。如果 fd 参数设置 AT_FDCWD,相当于调用当前工作目录路径。如果 pathname 参数是绝对路径名,fd 参数被忽略。

如果 pathname 是符号链接,那么 unlink 删除该符号链接,而不是删除由该链接所引用的文件。给出符号链接名的情况下,没有一个函数能删除由该链接所引用的文件。

如果文件系统支持,超级用户可以调用 unlink 删除一个指定目录,但通常应当使用 rmdir 函数。

flag 参数设置为 AT_REMOVEDIR 标志时,unlinkat 函数功能类似于 rmdir 命令一样删除目录;反之则与 unlink 函数功能相同。

也可以用 remove 函数解除对一个文件或目录的链接。对于文件,remove 的功能与 unlink 相同。对于目录,remove 的功能与 rmdir 相同。

1
2
3
#include <stdio.h>
int remove(const char *pathname);
// 返回值:成功返回0;失败返回-1

删除文件链接,必须对包含该目录项的目录具有写和执行权限。如果对该目录设置了粘着位(SBIT),则对该目录必须具有写权限,并且具备下面三个条件之一:

  • 拥有该文件
  • 拥有该目录
  • 具有超级用户权限

删除文件内容的两个条件:(1)当链接计数为0时,文件内容可以被删除 (2)没有进程打开了该文件,其内容才能被删除。

验证文件链接及删除文件内容示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#include "common/apue.h"

int main() {
int fd;
struct stat buf;
char *pathName = "/Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c";
char *newPathName = "/Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test_link.c";


// 第一步 测试未打开之前文件链接数
stat(pathName, &buf);
printf("1) link=%d\t%s\n", buf.st_nlink, pathName);

// 第二步 打开已存在的文件 测试链接数
fd = open(pathName, O_RDONLY);
stat(pathName, &buf);
printf("2) link=%d\t%s\n", buf.st_nlink, pathName);

// 第三步 关闭文件 测试链接数
close(fd);
stat(pathName, &buf);
printf("3) link=%d\t%s\n", buf.st_nlink, pathName);

// 第四步 创建硬链接 测试链接数
link(pathName, newPathName);
stat(pathName, &buf);
printf("4) link=%d\t%s\n", buf.st_nlink, pathName);

// 第五步 删除硬链接 测试链接数
unlink(newPathName);
stat(pathName, &buf);
printf("5) link=%d\t%s\n", buf.st_nlink, pathName);

// 第六步 打开已存在的文件
fd = open(pathName, O_RDONLY);

// 第七步 删除文件 测试链接数
// (此处我用 fstat 函数而非 stat,因为 unlilnk 已经删除文件名,所以不可以通过 文件名访问,
// 但是 fd 仍然是打开着的,文件内容还没有被真正删除,依旧可以使用 fd 获得文件信息。)
unlink(pathName);
fstat(fd, &buf);
printf("7) link=%d\t%s\n", buf.st_nlink, pathName);

// 第八步 关闭文件 文件内容被删除
close(fd);
}

// 返回结果
--------------------------------------------------------------------------
1) link=1 /Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c
2) link=1 /Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c
3) link=1 /Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c
4) link=2 /Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c
5) link=1 /Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c
7) link=0 /Users/windus/iwork/code/c_c++/algorithms_in_c/unix_test.c

unlink 这种特性经常被程序用来确保即使是在程序崩溃时,它所创建的临时文件也不会遗留下来。进程用 open 或 creat 创建一个文件,然后立即调用 unlink,因为该文件仍旧是打开的,所以不会将其内容删除。只有当进程关闭该文件或终止时(内核关闭该进程所打开的全部文件),该文件的内容才被删除。

函数renamerenameat

文件或目录可以用这两个函数进行重命名。

1
2
3
4
#include <stdio.h>
int rename(const char *oldname, const char *newname);
int renameat(int oldfd, const char *oldname, int newfd, const char *newname);
// 返回值:成功返回0;失败返回-1

ISO C 对文件定义了 rename 函数(C 标准不处理目录)。POSIX.1 扩展此定义,使其包含了目录和符号链接。

符号链接

符号链接是对一个文件的间接指针,它与硬链接有所不同,硬链接直接指向文件的 i 节点。引入符号链接的原因是为了避开硬链接的一些限制:

  • 硬链接通常要求链接和文件位于同一文件系统中。
  • 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)。

当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接,即该函数是否跟随符号链接到达它所链接的文件。下图中说明了各函数是否处理符号链接,图中没有列出 mkdir、mkinfo、mknod 和 rmdir 等函数,其原因是当路径名是符号链接时,它们都出错返回。以文件描述符作为参数的一些函数(如 fstat、fchmod等)也未列出,其原因是,对符号链接的处理是由返回文件描述符的函数(通常是 open )进行的。chown 是否跟随符号链接取决于实现,在所有现代的系统中,chown 函数都跟随符号链接。

符号链接由 4.2BSD 引入,chown 最初并不跟随符号链接,现在,所有平台都实现了 lchown,它可以改变符号链接自身的所有权。

各函数对符号链接的处理各函数对符号链接的处理

同时用 O_CREAT 和 O_EXCL 两者调用 open 函数时,若路径名引用符号链接,open 将出错返回,errno 设置为 EEXIST。这种处理方式的意图是堵塞一个安全性漏洞,以防止具有特权的进程被诱骗写错误的文件。

使用符号链接可能在文件系统中引入循环。大多数查找路径名的函数在这种情况发生时都出错返回,errno 值为 ELOOP。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建一个新目录
$ mkdir
# 创建一个0长度的文件
$ touch foo/a
# 创建一个符号链接
$ ls -s ../foo foo/testdir

# 查看目录结构
$ ls -l foo
-rw-r----- 1 sar 0 Jan 22 00:16 a
lrwxrwxrwx 1 sar 6 Jan 22 00:16 testdir -> ../foo

// 使用 Solaris 的标准函数 ftw(3) 以降序遍历文件结构,打印每个路径名,则输出是:
foo
foo/a
foo/testdir
foo/testdir/a
foo/testdir/testdir
foo/testdir/testdir/a
foo/testdir/testdir/testdir
foo/testdir/testdir/testdir/a
(更多行,直到函数出错返回,此时 errno 值为 ELOOP)

注意,Linux的 ftw 和 nftw 函数记录了所有看到的目录并避免多次得利处理一个目录,因此这两个函数不显示这种程序运行行为。

想要消除这种循环,可以用 lstat 代替 stat 以阻止它跟随符号链接; 也可以用 unlink 文件 foo/testdir,因为 unlink 也不跟随符号链接。
但是如果创建了一个构成这种循环的硬链接,那么就很难消除它。这就是为什么 link 函数不允许构造指向目录的硬链接的原因

创建符号链接

1
2
3
4
5
#include <unistd.h>

int symlink(const char *actualpath, const char *sympath);
int symlinkat(const char *actualpath, int fd, const char *sympath);
// 返回值:成功返回0;失败返回-1

函数创建一个指向 actualpath 的新目录项 sympath。在创建之前,并不要求 actualpath 已经存在。并且,actualpath 和 sympath 并不需要位于同一文件系统中。

symlinkat 函数与 symlink 函数功能类似,但 sympath 参数根据打开的文件描述符引用的目录(fd 参数)指定的,如果 sympath 参数指定的是绝对路径或者 fd 参数设置了 AT_FDCWD 值,那么 symlinkat 就等同于 symlink 函数。

读取符号链接

因为 open 函数跟随符号链接,所以需要有一种方法打开该链接本身,并读该链接中的名字的函数:

1
2
3
4
5
#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);
// 返回值:若成功,返回读取的字节数;若出错返回-1

文件的时间

SUS 2008年版为了提高 stat 结构中时间字段的精度,从原来的秒提高到秒加上纳秒。每个文件属性所保存的实际精度依赖于文件系统的实现。对于时间戳记录在秒级的文件系统来说,纳秒这个字段就会被填充为0。对于时间戳的记录精度高于秒级的文件系统来说,不足秒的值被转换成纳秒并记录在纳秒这个字段中。

每个文件维护3个时间字段:

字段 说明 例子 ls(1)选项
st_atim 文件数据的最后访问时间 read -u
st_mtim 文件数据的最后修改时间 write 默认
st_ctim i 节点状态的最后更改时间 chmod、chown -c

状态更改时间是该文件的 i 节点最后一次被修改的时间。包括一颗心文件的访问权限、更改用户ID、更改链接数等操作。因为 i 节点中的所有信息都是与文件的实际内容分开存放的,所以需要单独记录更改 i 节点中信息的时间。系统并不维护对一个 i 节点的最后一次访问时间,所以 access 和 stat 函数并不更改这3个时间中的任何一个。

更多关于文件时间4

函数futimensutimensatutimes

修改文件的访问和修改时间可以使用这几个函数。其中 futimens 和 utimensat 函数可以指定纳秒级精度的时间戳。

1
2
3
4
5
#include <sys/stat.h>

int futimens(int fd, const struct timespec times[2]);
int utimensat(int fd, const char *path, const struct timespec times[2], int flag);
// 返回值:成功返回0;失败返回-1

这两个函数times数组参数的第一个元素包含访问时间,第二元素包含修改时间。这两个时间是日历时间,自特定时间(1970年1月1日 00:00:00)以来所经过的秒数。不足秒的部分用纳秒表示。
时间戳可以按下列4种方式之王进行指定:

  1. 如果 times 参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。
  2. 如果 times 参数指向两个 timespec 结构的数组,任数组元素的 tv_nsec 字段的值为 UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的 tv_sec 字段。
  3. 如果 times 参数指向两个 timespec 结构的数组,任一数组元素的 tv_nsec 字段值为 UTIME_OMIT,相应的时间戳保持不变,忽略相应的 tv_sec 字段。
  4. 如果 times 参数指向两个 timespec 结构的数组,且 tv_nsec 字段的值为既不是 UTIME_NOW 也不是 UTIME_OMIT,相应的时间戳设置为相应的 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,就不执行任何的权限检查。

flag参数设置 AT_SYMLINK_NOFOLLOW 标志,则符号链接本身的时间会被修改(如果路径名指向的是符号链接)。默认行为是跟随符号链接,并把文件的时间改成符号链接的时间。

上面两个函数都包含在 POSIX.1 中,第3个函数包含在 SUS 的 XSI 扩展选项中:

1
2
3
4
#include <sys/time.h>

int utimes(const char *pathname, const struct timeval times[2]);
// 返回值:成功返回0;失败返回-1

times参数包含两个时间戳(访问时间和修改时间),元素的数组的指针,两个时间戳是用秒和微秒表示的。

1
2
3
4
struct timeval {
time_t tv_sec; /* secons */
long tv_usec; /* microseconds */
}

函数mkdirmkdiratrmdir

用 mkdir 和 mkdirat 函数创建一个新的空目录。其中,...目录项是自动创建的。

1
2
3
4
5
#include <sys/stat.h>

int mkdir(const char *pathname, mode_t mode);
int mkdirat(int fd, const char *pathname, mode_t mode);
// 返回值:成功返回0;出错返回-1

早期 UNIX 版本并没有 mkdir 函数,进程调用 mknod 函数创建一个新目录,但是只有超级用户进程才能使用 mknod 函数。

用 rmdir 函数可以删除一个空目录(空目录只包含...目录)。

1
2
3
4
#include <unistd.h>

int rmdir(const char *pathname);
// 返回值:成功返回0;失败返回-1

读文件

早期系统每个目录项是16个字节,其中14个字节是文件名,2个字节是 i 节点编号。而对于4.2BSD,由于它允许更长的文件名,所以每个目录项的长度是可变的。因此,读目录的程序与系统相关,为了简化读目录 UNIX 包含了一套与目录有关的例程,它们是 POSIX.1 的一部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <dirent.h>

DIR *opendir(const char *pathname);
DIR *fdopendir(int fd);
// 返回值:成功返回指针;失败返回 NULL

struct dirent *readdir(DIR *dp);
// 返回值:成功返回指针;若在目录尾或出错返回 NULL

void rewinddir(DIR *dp);
int closedir(DIR *dp);
// 返回值:成功返回0;失败返回-1

long telldir(DIR *dp);
// 返回值:与 dp 关联的目录中的当前位置

void seekdir(DIR *dp, long loc);

DIR 结构是一个内部结构,上述7个函数用这个内部结构保存当前正在被读的目录的有关信息。

由 opendir 和 fdopendir 返回的指向 DIR 结构的指针由另外5个函数使用。opendir 执行初始化操作,使第一个 readdir 返回目录中的第一个目录项。当 DIR 结构由 fdopendir 创建时,readdir 返回的第一项取决于传给 fdopendir 函数的文件描述符相关联的文件偏移量。

目录中各目录的顺序与实现有关,它们通常并不按字母顺序排序。

函数chdirfchdirgetcwd

每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点。当用户登录到 UNIX 系统时,其当前工作目录通常是口令文件( /etc/passwd )中该用户登录项的第6个字段——用户的起始目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。

1
2
3
4
5
#include <unistd.h>

int chdir(const char *pathname);
int fchdir(int fd);
// 返回值:成功返回0;失败返回-1

进程调用这两个函数可以更改当前工作目录。因为当前目录是进程的一个属性,所以它只影响调用 chdir 的进程本身,而不影响其他进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include "apue.h"

int main(void)
{
if (chdir("/tmp") < 0)
err_sys("chdir failed");
printf("chdir to /tmp succeeded\n");
exit(0);
}

// 调用程序测试
$ pwd
/usr/lib
$ mycd
chdir to /tmp succeeded
$ pwd
/usr/lib

从上示例代码中执行程序后,shell 的当前工作目录并没有改变。因为每个程序运行在独立的进程中,shell 的当前工作目录并不会随着程序调用 chdir 而改变。

系统内核并不保存该目录的完整路径名,只为每个进程只保存指向该目录 v 节点的指针等目录本身的信息。可以通过 getcwd 函数获取:

1
2
3
4
#include <unistd.h>

char *getcwd(char *buf, size_t size);
// 返回值:成功返回 buf;失败返回 NULL

该函数必须传递两个参数,一个是缓冲区地址 buf,该缓冲区必须有足够长度以容纳绝对路径名再加上一个终止 null 字节,否则返回出错;另一个是缓冲区长度 size(以字节为单位)。