前言
近期学习了一下UNIX环境高级编程,也做了一些相关知识点的笔记,以便后续自己的回忆以及阅读,同时方便读者的理解与应用。这对将来我们的嵌入式驱动代码的编写将会有很大的裨益,欢迎指出不足,择优修改,共勉成长!
目录
4.1 函数stat、fstat、fstatat和lstat
4.2.3 块特殊文件(block special file)
4.2.4 字符特殊文件(character special file)
4.10 函数chown、fchown、fchownat 和 lchown
4.14 函数link、linkat、unlink、unlinkat和remove
4.19 函数futimens、utimensat和utimes
4.1 函数stat、fstat、fstatat和lstat
返回指定文件信息。
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 dirfd, const char *restrict pathname, struct stat *restrict buf, int flag);
//所有4个函数的返回值:若成功:返回0;若出错,返回-1
- stat函数返回pathname指定的文件信息,将文件信息保存在buf参数中。
- fstat函数与stat类似,只是通过文件描述符fd标识文件。
- lstat与stat类似,只是当pathname是符号链接时,获得的是符号链接文件本身信息,而不是符号链接引用的文件的信息。
- fstatat中,flag参数控制着是否跟随一个符号链接。当AT_SYMLINK_NOFOLLOW标志被设置时,fstatat不会跟随符号链接,而是返回符号链接本身的信息。否则在默认情况下,返回的是符号链接所指向的实际文件的信息。若fd为AT_FDCWD,而且pathname为相对路径名,fstata会计算相对于当前目录下的pathname参数。如果pathname为绝对路径,fd被忽略。这两种情况下根据flag的取值,fstatat的作用与stat和lstat一样。
- buf为一个指针,指向我们提供的结构。其基本形式如下:
struct stat {
mode_t st_mode; /* 文件类型和访问权限 */
ino_t st_ino; /* i-node number */
dev_t st_dev; /* 文件系统设备号 */
dev_t st_rdev; /*特殊文件系统设备号*/
nlink_t st_nlink; /* 硬链接个数 */
uid_t st_uid; /* 所有者uid */
gid_t st_gid; /* 所有者gid */
off_t st_size; /* 文件大小(字节) */
struct timespec st_atime; /* 文件数据最近访问时间 */
struct timespec st_mtime; /* 文件数据最近修改时间 */
struct timespec st_ctime; /* inode节点状态最近改变时间 */
blksize_t st_blksize; /* 对文件I/O较合适的块长度 */
blkcnt_t st_blocks; /* 该文件分配的512字节块的数量 */
};
注意:timespec结构类型按照秒和纳秒定义了时间,至少包括了以下两个字段:
time_t tv_sec;
long tv_nsec;
使用stat最多的地方就是ls -l命令,用其可以获得有关一个文件的所有信息。
4.2 文件类型
UNIX系统的大多数文件是普通文件或者目录,但是也有其他另外一些文件类型。
4.2.1 普通文件(regular file)
最常用的文件类型,包含了某种形式的数据。至于数据是文本还是二进制形式对Unix环境来说并无区别。
4.2.2 目录文件(directory file)
这种文件包含了其他文件的名字以及指向与这些文件有关信息的指针。对一个目录文件具有读权限的任意进程都可以读该目录的内容,但是只有内核可以直接写目录文件。
4.2.3 块特殊文件(block special file)
提供对设备(如磁带)带缓冲的访问,每次访问以固定长度为单位进行。
4.2.4 字符特殊文件(character special file)
提供对设备不带缓冲的访问,每次访问长度可变。系统中的所有设备要么是字符特殊文件,要么是块特殊文件。
4.2.5 FIFO
这种类型的文件用于进程间通信,有时也称为命名管道
4.2.6 套接字(socket)
这种类型的文件用于进程间的网络通信,也可以用于在一台宿主机上进程间的非网络通信。
4.2.7 符号链接(symbolic link)
这种类型文件指向另一个文件,类似于快捷方式。
注意:文件类型信息包含在stat结构的st_mode成员中。以下宏的参数都是stat结构中的st_mode成员。
S_ISREG() | 普通文件 |
S_ISDIR() | 目录文件 |
S_ISCHR() | 字符特殊文件 |
S_ISBLK() | 块特殊文件 |
S_ISFIFO() | 管道或FIFO |
S_ISLNK() | 符号链接 |
S_ISSOCK() | 套接字 |
以下程序取其命令行参数,然后针对每一个命令行参数打印其类型。
#include "apue.h"
int main(char argc,char *argv[])
{
int i;
sturct stat buf;
char *ptr;
for(i=1;i<argc;i++)
{
printf("%s:",arfv[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 = "character special";
}
else if(S_ISBLK(buf.st_mode))
{
ptr = "block special";
}
else if(S_ISFIFO(buf.st_mode))
{
ptr = "fifo";
}
else if(S_ISLINK(buf.st_mode))
{
ptr = "symbol link";
}
else if(S_ISSOCK(buf.st_mode))
{
ptr = "socket";
}
else
{
ptr = "** unknown mode ** ";
}
printf("%s\n",ptr);
}
exit(0);
}
程序输出结果为:
在此处我们使用的是lstat函数而不是stat函数以防检测不到符号链接,符号链接的概念我们会在下文进行讲解。
$./a.out/etc/passwd /etc /dov/log /dev/tty \
>/var/lib/oprofile/opd_pipe /dev/sr0 /dev/cdrom /etc/passwd: reqular
/etc: directory
/dev/log: socket
/dev/tty:character special
/var/lib/oprofile/opd.pipe:fifo
/dev/sr0:block special
/dev/cdrom: symbolic link
4.3 设置用户ID和设置组ID
与一个进程相关联的ID有六个甚至更多,如下表格所示:
实际用户ID 实际组ID | 我们实际上是谁,即执行此进程的实际用户。 这两个ID是用户登录时取自口令文件的。 | |
---|---|---|
有效用户ID 有效组ID 附属组ID | 用于文件访问权限检查。 | |
保存的设置用户ID 保存的设置组ID | 用exec函数保存。 |
- 实际用户ID和实际用户组ID的话超级进程是有方法改变它们的。
- 有效用户ID和有效用户组ID决定了我们的文件访问权限
- 保存的设置用户ID和保存的设置组ID在执行一个程序时包含了有效用户ID和有效组ID的副本
例如,若文件所有者是超级用户,而且设置了该文件的设置用户ID位,那么当该程序文件由一个进程执行时,该进程具有超级用户权限。
例如,UNLX系统程序passwd(1)允许任一用户改变其口令,该程序是一个设置用户D程序。因为该程序应能将用户的新口令写入口令文件中(一般是/etc/passwd或/etc/shadow),而只有超级用户才具有对该文件的写权限,所以需要使用设置用户ID功能。因为运行设置用户ID程序的进程通常会得到额外的权限,所以编写这种程序时要特别谨慎。
4.4 文件访问权限
st_mode值也包含了对文件的访问权限位。
每个文件有9个访问权限位,可将它们分成3类:
st_mode屏蔽 | 含义 |
S_IRUSR | 用户读 |
S_IWUSR | 用户写 |
S_IXUSR | 用户执行 |
S_IRGRP | 组读 |
S_IWGRP | 组写 |
S_IXGRP | 组执行 |
S_IROTH | 其他读 |
S_IWOTH | 其他写 |
S_IXOTH | 其他执行 |
其实很好理解和记忆R代表read、W代表wirte、X代表执行、USR代表USER(也就是用户)、GRP代表group(也就是用户组的概念)。对照上表记忆即可。
以下讨论几个规则:
- 我们用名字打开任一类型的文件时,对该名字中包含的每一个目录,包括它可能隐含的当前工作目录都应具有执行权限。这就是为什么对于目录其执行权限位常被称为搜索位的原因。例如,为了打开文件/usr/include/stdio.h,需要对目录/、/usr和/usr/include具有执行权限。然后,需要具有对文件本身的适当权限,这取决于以何种模式打开它(只读、读-写等)。在当前目录是/usr/include的情况下打开stdio.h文件与打开./stdio.h作用相同。如果PATH环境变量(8.10节将对其进行说明)指定了一个我们不具有执行权限的目录,那么shell绝不会在该目录下找到可执行文件。
- 对于一个文件的读权限决定了我们能否打开它进行读操作,与open函数的O_RDONLY和O_RDWR标志相关
- 对于一个文件的读权限决定了我们能否打开它进行读操作,与open函数的O_RDONLY和O_RDWR标志相关
- 在open中对一个文件指定O_TRUNC标志,需要拥有该文件的写权限
- 在open中对一个文件指定O_TRUNC标志,需要拥有该文件的写权限
-
在open中对一个文件指定O_TRUNC标志,需要拥有该文件的写权限
-
如果用exec函数族执行某一文件,需要对该文件的执行权限,且该文件必须是普通文件类型。
文件访问权限测试流程: (按顺序执行以下四步)
4.5 新文件和目录的所有权
新目录的所有权规则与新文件所有权规则相同
- 新文件的用户ID设置为进程的有效用户ID
- 新文件的组ID设置为进程的有效组ID或该文件所在目录的组ID。
4.6 函数access和faccessat
这两个函数说到底就是测试文件的属性以及权限位,把文件描述符放进去,测就完了,看下面操作你就懂了。
两个函数的具体形式:
#include <unistd.h>
int access(const char *pathname,int mode);
int faccessat(int fd, const char *pathname,int mode,int flag);
//两个函数的返回值:若成功,返回0:若出错,返回-1
mode | 说明 |
R_OK | 测试读权限 |
W_OK | 测试写权限 |
X_OK | 测试实行权限 |
F_OK | 判断文件存在与否 |
4.7 函数umask
#include <ays/stat.h>
mode_t umask(mode_t cmask);
//返回值:之前的文件模式创建屏蔽字
这个函数的用法实际上就是创建屏蔽字,通俗一点来说,我创建文件前设一个umask函数在前面,在函数里面传入我要屏蔽的权限位(比如用户读、组写、其他堵写等,以此类推)。然后我再去设置这个文件对应位的读写执行权限就不行了。讲理论太枯燥了,上实战(只写部分代码,博主太累了o.O):
umask(0); // 没有屏蔽任何权限位
creat("foo",RWRWRW); // 我建一个文件为用户读写组读写其他用户读写
umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH); // 屏蔽掉了组读和组写权限位以及其他用户读写权限位
creat("bar.txt",RWRWRW); // 我再建一个文件作对比(注意其中四个权限位被屏蔽掉了)
看结果:
$ ./a.out
$ ls -l foo bar
-rw------- 1 bar 0 Dec 7 21:20 bar
-rw-rw-rw- 1 foo 0 Dec 7 21:20 foo
这回清楚了吧,我把bar文件的那几个位屏蔽掉之后,天王老子来设置也没有用!所以这函数还是很妙的。对了,友友们还可以根据数字来设置屏蔽位哟(差点忘了o.O),例如这样umask(0400)我把用户读给你屏了。
屏蔽位 | 含义 |
0400 | 用户读 |
0200 | 用户写 |
0100 | 用户执行 |
0040 | 组读 |
0020 | 组写 |
0010 | 组执行 |
0004 | 其他读 |
0002 | 其他写 |
0001 | 其他执行 |
4.8 函数chmod、fchmod和fchmodat
这三个函数更改我们现有文件的访问权限。
#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);
//3个函数返回值:若成功,返回0:若出错。返回-1
chmod函数在指定的文件上进行操作,而fchmod函数则对已打开的文件进行操作。 chmodat函数与chmod函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。否则,fchmodat计算相对于打开目录(由fd参数指向)的pathname。flag参数可以用于改变fchmodat的行为,当设置了AT_SYMLINK_NOFOLLOW标志时,fchmodat并不会跟随符号链接。
mode | 说明 |
S_ISUID S_ISVTX | 执行时设置用户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 | 其他读写执行权限 其他读权限 其他写权限 其他执行权限 |
注意:chmod函数更新的只是i节点(第三章的内容,赶紧回去看看)最近一次被更改的时间。按系统默认方式,ls -l列出的是最后修改文件内容的时间。
4.9 粘着位(nian还是zhan随你们吧o.O)
S_ISVTX位被称为粘着位(sticky bit)。如果一个可执行程序文件的这一位被设置了,那么当该程序第一次被执行,在其终止时,程序正文部分的一个副本仍被保存在交换区(程序的正文部分是机器指令)。这使得下次执行该程序时能较快地将其装载入内存。现今较新的UNLX系统大多数都配置了虚拟存储系统以及快速文件系统,所以不再需要使用这种技术。
但是!!!现今的系统扩展了粘着位的使用范围,Single UNLX Specificaton允许针对目录设置粘着位。如果对一个目录设置了粘着位,只有对该目录具有写权限的用户并且满足下列条件之一,才能删除或重命名该目录下的文件:
- 拥有此文件;
- 拥有此目录;
- 是超级用户。
4.10 函数chown、fchown、fchownat 和 lchown
下面几个chown函数可用于更改文件的用户ID和组ID。如果两个参数owner或group中的任意一个是-1,则对应的ID不变。
#include <unistd.h>
int chown(const char *pathmame,uid_t owmer,gid_t group);
int fchown(int fd,uid_t owner,gid_t growp);
int fchownat(int fd,const char *pathmame,uid_t owmer,gid_t group,int flag);
int lchown(const char *pathmame,uid_t owmer,gid_t grop);
//4个函数的返回值:若成功,返回0:若出错,返回-1
除了所引用的文件是符号链接以外,这4个函数的操作类似。在符号链接情况下,1chown和fchownat(设置了AT_SYMLINK_NOFOLLOW标志)更改符号链接本身的所有者,而不是该符号链接所指向的文件的所有者。
fchown函数改变fd参数指向的打开文件的所有者,既然它在一个已打开的文件上操作,就不能用于改变符号链接的所有者。
fchownat函数与chown或者1chown函数在下面两种情况下是相同的:一种是pathname参数为绝对路径,另一种是fd参数取值为AT_FDCWD而pathname参数为相对路径。在这两种情况下,如果flag参数中设置了AT_SYMLINK_NOFOLLOW标志,fchownat与lchown行为相同,如果flag参数中清除了AT_SYMLINK_NOFOLLOW标志,则fchownat与chown行为相同。如果fd参数设置为打开目录的文件描述符,并且pathname参数是一个相对路径名,fchownat函数计算相对于打开目录的pathname。
若_POSIX_CHOWN_RESTRICTED对指定的文件生效,则(其实也就是我们现在普遍用的系统里面的规则)
1. 只有超级用户进程能更改该文件的用户ID
2. 如果进程拥有此文件(其有效用户ID等于该文件的用户ID),参数owner等于-1或文件的用户ID,并且参数group等于进程的有效组ID或进程的附属组ID之一,那么一个非超级用户进程可以更改该文件的组ID。
4.11 文件长度(涉及到了文件空洞)
4.11.1 文件长度
stat结构成员st_size表示以字节为单位的文件的长度。此字段只对普通文件、目录文件和符号链接有意义。
- 对于普通文件,其文件长度可以是0,在开始读这种文件时,将得到文件结束(end-of-file)指示。对于目录,文件长度通常是一个数(如16或512)的整倍数。
- 对于符号链接,文件长度是在文件名中的实际字节数。例如,在下面的例子中,文件长度7就是路径名usr/lib的长度:
lrwxrwxrwx 1 root 7 Sep 2507:14 lib -> usr/lib
现今,大多数现代的UNLX系统提供字段st_blksize和st_blocks。其中,第一个是对文件I/O较合适的块长度,第二个是所分配的实际512字节块块数。回忆3.9节,其中提到了当我们将st_blksize用于读操作时,读一个文件所需的时间量最少。为了提高效率,标准I/O库(我们将在第5章中说明)也试图一次读、写st_blksize个字节。
4.11.2 文件空洞
$ ls -l core
-rw-r--r-- 1 sar 8483248 Nov 18 12:18 core
$ du -s core
在很多BSD类系统上,du命今报告的是1024字节块的块数,Solaris报告的是512字节块的块数。在Linux上,报告的块数单位取决于是否设置了环境变量POSIXLY_CORRECT。当设置了该环境变量,du命令报告的是1024字节块的块数;没有设置该环境变量时,du命令报告的是512字节块的块数。
对于没有写过的字节位置,read函数读到的字节是0。如果执行下面的命令,可以看出正常的I/O操作读整个文件长度:
$ wc -c core
8483248 core
//带-c选项的wc(1)命令计算文件中的字符数(字节)。
如果使用实用程序(如cat(1))复制这个文件,那么所有这些空洞都会被填满,其中所有实际数据字节皆填写为0。
$ cat core> core.copy
$ ls -1 core*
-rw-r--r--1 sar 8483248 Nov 1812;18 core
-rw-rw-r--1 sar 8483248 Nov 1812:27 core.copy
$ du -s core*
//结果
272 core
16592 core.copy
4.12 文件截断
有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例,在打开文件时使用O_TRUNC标志可以做到这一点。为了截断文件可以调用函数truncate和ftruncate。
#include <unistd.h>
int truncate(const char*pathmame,off_t length);
int ftruncate(int fd,off_t length);
//两个函数的返回值:若成功,返回0:若出错,返回-1
这两个函数将一个现有文件长度截断为length。如果该文件以前的长度大于length,则超过length以外的数据就不再能访问。如果以前的长度小于length,文件长度将增加,在以前的文件尾端和新的文件尾端之间的数据将读作0(也就是可能在文件中创建了一个空洞)。
4.13 文件系统(嫌累可以滑到本小结末看文件系统的总结)
我们可以把一个磁盘分成一个或多个分区。每个分区可以包含一个文件系统(见图4-13)。i(也可以叫做inode节点)节点是固定长度的记录项,它包含有关文件的大部分信息。
- 在下图中有两个目录项指向同一个i节点。每个i节点中都有一个链接计数,其值是指向该 i节点的目录项数。只有当链接计数减少至0时,才可删除该文件(也就是可以释放该文件占用的数据块)。 这就是为什么 “解除对一个文件的链接”操作并不总是意味着“释放该文件占用的磁盘块”的原因。这也是为什么删除一个目录项的函数被称之为 unlink而不是 delete 的原因。在stat结构中,链接计数包含在st_nlink成员中,其基本系统数据类型是nlin k_t。 这 种链接类型称为硬链接。
- 另外一种链接类型称为符号链接(symbolic link)。符号链接文件的实际内容(在数据块中)包含了该符号链接所指向的文件的名字。在下面的例子中,目录项中的文件名是3个字符的字符串lib,而在该文件中包含了7个字节的数据usr/lib:
lrwxrwxrwx l root 7 Sep 2507:14 lib -> urs/lib
- 该i节点中的文件类型是S_IFLNK,于是系统知道这是一个符号链接。
- 节点包含了文件有关的所有信息:文件类型、文件访问权限位、文件长度和指向文件据块的指针等。stat结构中的大多数信息都取自i节点。只有两项重要数据存放在目录项中:文件名和i节点编号。
- 因为目录项中的i节点编号指向同一文件系统中的相应i节点,一个目录项不能指向另一个文件系统的i节点。这就是为什么ln(1)命令(构造一个指向一个现有文件的新目录项)不能跨越文件系统的原因。我们将在下一节说明link函数。
- 当在不更换文件系统的情况下为一个文件重命名时,该文件的实际内容并未移动,只需 构造一个指向现有i节点的新目录项,并删除老的目录项。链接计数不会改变。例如,为将文件/usr/lib/foo重命名为/usr/foo,如果目录/usr/lib和/usr在同一文件系统中,则文件foo的内容无需移动。这就是mv(1)命令的通常操作方式。
目录文件的链接计数字段:
假定我们在工作目录中构造了一个新目录:
$ mkdir testdir
编号为2549的i节点,其类型字段表示它是一个目录,链接计数为2。任何一个叶目录(不包含任何其他目录的目录)的链接计数总是2,数值2来自于命名该目录(testdir)的目录项以及在该目录中的.项。编号为1267的i节点,其类型字段表示它是一个目录,链接计数大于或等于3。它大于或等于3的原因是,至少有3个目录项指向它;一个是命名它的目录项(在图4-15中没有表示出来),第二个是在该目录中的.项,第三个是在其子目录testdir中的..项。注意,在父目录中的每一个子目录都使该父目录的链接计数增加1。
小结(个人理解):其实文件系统讲的就是文件磁盘等一些关系,像我们windows计算机有C盘、D盘、E盘,这就是一个一个的磁盘分区。分区里面有什么,肯定有文件啊,数据啊,那么就有i节点来指向文件的相关信息,什么长度啊、信息啊、能不能读写啊都在i节点里面。数据理所当然就应该在数据块啦。这时候问题来了,如果我把一个文件复制一个一模一样的到这个磁盘下,会不会重新分配数据块给这个文件包含的数据呢?理所当然不会,这样多浪费啊。那么我要用这个文件的数据怎么办呢,i节点链接计数+1不就行了。也就是说有很多个链接指向我这个文件的i节点,都可以用这里面的数据,其实也就类似于副本,这样说就能理解了吧,后文也会提到符号链接,这个符号链接就相当于是我们电脑的快捷方式。你如果想彻底删除文件相关数据块,就得把所有副本和源文件删除掉,i节点链接数不就清零了嘛。其实日常操作电脑的时候我们可以发现,在同一个磁盘分区下,新建一个副本会很快,但是当我们把c盘分区的文件复制到d盘分区,用的时间真的长太多,这就是文件系统里面的区别。
4.14 函数link、linkat、unlink、unlinkat和remove
如上节所述,任何一个文件可以有多个目录项指向其i节点。创建一个指向现有文件的链接的方法是使用link函数或linkat函数。
#include <unistd.h>
int link(const char *existingpath,const char *newpath);
int linkat(int efd,const char *existingpath,int nd,const char *newpath,int flag);
//两个函数的返回值:若成功,返回0:若出错,返回-1
这两个函数创建一个新目录项newpath,它引用现有文件existingpath。如果newpath已经存在,则返回出错。只创建newpath中的最后一个分量,路径中的其他部分应当已经存在。
对于linkat函数,现有文件是通过efd和existingpath参数指定的,新的路径名是通过nfd和newpath参数指定的。默认情况下,如果两个路径名中的任一个是相对路径,那么它需要通过相对于对应的文件描述符进行计算。如果两个文件描述符中的任一个设置为AT_FDCWD,那么相应的路径名(如果它是相对路径)就通过相对于当前目录进行计算。如果任一路径名是绝对路径,相应的文件描述符参数就会被忽略(和大部分函数大同小异了)。
如果在flag参数中设置了ATSYMLINK_FOLLOW标志,就创建指向符号链接目标的链接。如果这个标志被清除了,则创建一个指向符号链接本身的链接。
创建新目录项和增加链接计数应当是一个原子操作。
#include <unistd.h>
int unlink(const char *pathname);
int unlinkat(int fd,const char *pathmame,int fag);
//两个函数的返回值:若成功,返回0:若出错,返回-1
只有当链接计数达到0时,该文件的内容才可被删除。另一个条件也会阻止删除文件的内容——只要有进程打开了该文件,其内容也不能删除。关闭一个文件时,内核首先检查打开该文件的进程个数;如果这个计数达到0,内核再去检查其链接计数;如果计数也是0,那么就删除该文件的内容(其实也就类似于我们平常打开了一个文件,然后又要去删除他,删不掉,把文件关掉就行了)。
如果pathname参数是相对路径名,那么unlinkat函数计算相对于由fd文件描述符参数代表的目录的路径名。如果fd参数设置为AT_FDCWD,那么通过相对于调用进程的当前工作目录来计算路径名。如果pathname参数是绝对路径名,那么fd参数被忽略(和大多数函数一样)。
fag参数给出了一种方法,使调用进程可以改变unlinkat函数的默认行为。AT_REMOVEDIR标志被设置时,unlinkat函数可以类似于rmdir一样删除目录。如果这个标志被清除,unlinkat与unlink执行同样的操作。
重点来了!!!
unlink 命令在同一时间只能删除一个文件或链接,而 rm 命令可以删除多个;unlink 命令不能删除目录,而 rm 命令能删除目录。
rm 命令在执行的时候,首先会安全检查,如果你没有文件的写权限,那么系统会要求你给出写权限(sudo或者切换至管理员用户),或者使用强制删除选项 -f;而 unlink 则不会进行安全检查,直接删除文件。
在某些情况下,相比于 rm,你可能更喜欢使用 unlink。比如你希望强制删除一个文件,而不考虑安全或者权限问题;或者如果删除失败(比如文件不存在)的话你希望能看到报错信息,这种情况下就可以使用 unlink。因为使用 rm -f 强制删除文件,如果文件不存在的话,不会显示任何错误信息。
4.15 函数rename和renameat
用于文件或目录重命名
其实这个博主讲的挺好的,可以去他那看看这两个函数的具体用法。
C语言rename()函数:重命名文件或目录_c语言 rename_GoRustNeverStop的博客-优快云博客
#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
- 如果oldname指的是一个文件而不是目录,那么为该文件或符号链接重命名。在这种情况下,如果newname已存在,则它不能引用一个目录。如果newname已存在,而且不是一个目录,则先将该目录项删除然后将oldname重命名为newname。对包含oldname的目录以及包含newname的目录,调用进程必须具有写权限,因为将更改这两个目录。
- 如若oldname指的是一个目录,那么为该目录重命名。如果newname已存在,则它必须引用一个目录,而且该目录应当是空目录(空目录指的是该目录中只有.和..项)。如果newname存在(而且是一个空目录),则先将其删除,然后将oldname重命名为newname。另外,当为一个目录重命名时,newname不能包含oldname作为其路径前缀。例如,不能将/usr/foo重命名为/usr/foo/testdir,因为旧名字(/usr/foo)是新名字的路径前缀,因而不能将其删除。
- 如若oldname或newname引用符号链接,则处理的是符号链接本身,而不是它所引用的文件。
- 不能对.和..重命名。更确切地说,.和..都不能出现在oldname和newname的最后部分。
- 作为一个特例,如果oldname和newmame引用同一文件,则函数不做任何更改而成功返回。
4.16 符号链接(快捷方式来了)
- 硬链接通常要求链接和文件位于同一文件系统中。
- 只有超级用户才能创建指向目录的硬链接(在底层文件系统支持的情况下)。
有个图有点重要,可以看看。

4.17 创建和读取符号链接
可以用symlink或symlinkat函数创建一个符号链接。实际上ln -s也可以创建符号链接,但是单独的ln就只能创建硬链接了。
关于符号链接(软链接)和硬链接这个博主写的不错,较为详细,看完记得回来!
什么是软链接、硬链接_king config的博客-优快云博客
#include <unistd.h>
int symlink(const char *actuapath,const char *sympath);
int symlinkat(const char *acfualpath,int fd,const char *sympath);
//两个函数的返回值:若成功,返回0:若出错,返回-1
函数创建了一个指向actualpath的新目录项symyparh。在创建此符号链接时,并不要求achualpath已经存在。并且,actualpath和sympath并不需要位于同一文件系统中。
#include <unistd.h>
ssize_t readlink(const char *restrict pathname,char *restrict buf,size_t bufsize)
ssize_t readlinkat(int fd,const char* restrict pathmame,char *restrict buf,size_t bufsie);
//两个函数的返回值:若成功,返回读取的字节数;若出错,返回-1
4.18 文件的时间
stat结构体中的st_atim、st_mtim、st_ctim成员即为文件时间信息
每个文件属性所保存的实际精度依赖于文件系统的实现。 对于把时间戳记录在秒级的文件系统来说,纳秒这个字段就会被填充为 0。 对于时间戳的记录精度高于秒级的文件系统来说,不足秒的值被转换成纳秒并记录在纳秒这个字段中。
字段 | 说明 | 例子 | ls选项 |
s
t
_
a
t
i
m
|
文件数据的最后访间时间
|
r
ea
d
|
-
u
|
st_mtim |
文件数据的最后修改时间
|
w
r
i
t
e
|
默认
|
st
_
c
t
i
m
|
i节点状态的最后更改时间
|
c
h
m
o
d
、c
h
o
w
n
|
-
C
|
ls命令按这3个时间值中的一个排序进行显示。系统默认(用-1或-t选项调用时)是按文件的修改时间的先后排序显示。-u选项使1s命令按访问时间排序,-c选项则使其按状态更改时间排序。
图4-20列出了我们已说明过的各种函数对这3个时间的作用。目录是包含目录项(文件名和相关的i节点编号)的文件,增加、删除或修改目录项会影响到它所在目录相关的3个时间。这就是在图4-20中包含两列的原因,其中一列是与该文件(或目录)相关的3个时间,另一列是与所引用的文件(或目录)的父目录相关的3个时间。例如,创建一个新文件影响到包含此新文件的目录,也影响该新文件的i节点。但是,读或写一个文件只影响该文件的i节点,而对目录则无影响(我觉得读文件之所以影响到i节点可能是因为文件信息里面的访问时间变了,但是下图并没有显示出来,所以这个地方是存在歧义的)。
4.19 函数futimens、utimensat和utimes
#include <sys/stat.h>
int futimens(int fd,const struct timespec times[2]);
int utimensat(intjd,const char *path,const struct timespec times[2],int flag);
//两个函数返回值:若成功,返回0:若出错,返回-1
这两个函数的times数组参数的第一个元素包含访问时间,第二元素包含修改时间。这两个时间值是日历时间,如1.10节所述,这是自特定时间(1970年1月1日00:00:00)以来所经过的秒数。不足秒的部分用纳秒表示(感觉看到这就行了,后面的修改时间遇到问题再来差也是一样的)。
- 如果times参数是一个空指针,则访问时间和修改时间两者都设置为当前时间。
- 如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_NOW,相应的时间戳就设置为当前时间,忽略相应的tv_sec字段。
- 如果times参数指向两个timespec结构的数组,任一数组元素的tv_nsec字段的值为UTIME_OMIT,相应的时间戳保持不变,忽略相应的tv_sec字段。
- 如果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,就不执行任何的 权限检查。
#include <sys/time.h>
int utimes(const char *pathname,const struct timeval times[2]);
//函数返回值:若成功,返回0;若出错,返回-1
utimes函数对路径名进行操作。times参数是指向包含两个时间戳(访问时间和修改时间)元素的数组的指针,两个时间戳是用秒和微妙表示的。
struct timeval {
time_t tv_sec;/* seconds */
long tv_usec;/* microseconds */
};
4.20 函数mkdir、mkdirat和rmdir
用mkdir和mkdirat函数创建目录,用rmdir函数删除目录。
#include <sys/stat.h>
int mkdir(const char *pathname,mode_t mode);
int mkdirat(int fd,const char *pathname,mode_t mode);
//两个函数返回值:若成功,返回0;若出错,返回-1
这两个函数创建一个新的空目录。其中,.和..目录项是自动创建的。所指定的文件访问权限mode由进程的文件模式创建屏蔽字修改。
常见的错误是指定目录文件的读写权限,但是没有指定执行权限。对于目录文件来说,至少应该设置执行权限,以允许访问该目录中的文件,在BSD系统中,新目录的组ID是从父目录继承的。Solaris 10和Linux 3.2.0也使新目录继承父目录的设置组ID位。
用rmdir函数可以删除一个空目录。空目录是只包含.和..这两项的目录。
#include <unistd.h>
int rmdir(const char *pathname);
//返回值:若成功,返回0;若出错,返回-1
4.21 读目录
对于各个函数的解释就放在代码段里面了,这里不再做过多解释(目录流指代一大串目录,例如/temp/dev就是一个目录流)。
#include <dirent.h>
/*打开目录文件,返回一个指向该目录流的指针,该流被定位在目录的第一个条目上。目录中的每一个条目都指向该目录中的一个文件,目录中还有两个特殊的目录条目即".“和”…"分别是该目录文件和上一级目录文件。失败返回NULL*/
DIR *opendir(const char *parhname);
DIR *fdopendir(int fd);
//两个函数返回值:若成功,返回指针:若出错,返回NULL
//读取目录项,该结构代表了目录流中的下一个目录条目
struct dirent *readdir(DIR *dp);
//返回值:若成功,返回指针;若在目录尾或出错,返回NULL
// 关闭目录文件,关闭参数dir所指的目录流
int closedir(DIR *dp);
//返回值:若成功,返回0;若出错,返回-1
//设置目录流读取位置
void rewinddir(DIR *dp);//用来设置参数dir目录流读取位置为原来开头的读取位置
long telldir(DIR *dp);//函数的返回值记录着一个目录流的当前位置。此返回值代表距离目录文件开头的偏移量
void seekdir(DIR *dp,long loc);//用来设置参数dir目录流读取位置,在调用readdir()时便从此新位置开始读取。参数offset代表距离目录文件开头的偏移量
ino_t d_ino; /* i-node number */
char d_name[]; /* nuil-terminated filename */
重点叕来了!!!
由opendir和fdopendir返回的指向DIR结构的指针由另外5个函数使用。opendir执行初始化操作,使第一个readdir返回目录中的第一个目录项。DIR结构由fdopendir创建时,readdir返回的第一项取决于传给fdopendir函数的文件描述符相关联的文件偏移量。注意,目录中各目录项的顺序与实现有关。它们通常并不按字母顺序排列。
4.22 函数chdir、fchdir和getcwd
进程调用chdir或fchdir函数可以更改当前工作目录。 假如在目录A中运行了目录B中的程序,那么进程B的工作目录是目录A。
每个进程都有一个当前工作目录,此目录是搜索所有相对路径名的起点(不以斜线开始的路径名为相对路径名)。当用户登录到UNLX系统时,其当前工作目录通常是口令文件(/etc/passwd)中该用户登录项的第6个字段——用户的起始目录(home directory)。当前工作目录是进程的一个属性,起始目录则是登录名的一个属性。
#include <unistd.h>
int chdir(const char"pathname");
int fchdir(int fd);
//两个函数的返回值:若成功,返回0:若出错,返回-1
在这两个函数中,分别用pathname或打开文件描述符来指定新的当前工作目录。
#include "apue.h"
int main(void)
{
if(chdir("/tmp")<0)
{
err_sys("chdir failed");
}
printf("chdir to /tmp succeeded\n");
exit(0);
}
如果编译程序,并且调用其可执行目标代码文件mycd,则可以得到下列结果:
5 pwd
/usr/lib
$ mycd
chdir to /tmp succeeded
$ pwd
/usr/lib
从中可以看出,执行mycd命令的shell的当前工作目录并没有改变,这是shell执行程序工作方式的一个副作用。每个程序运行在独立的进程中,shell的当前工作目录并不会随着程序调用chdir而改变。由此可见,为了改变shell进程自己的工作目录,shell应当直接调用chdir函数,为此,cd命令内建在shell中。
通过getcwd函数获取调用进程的当前工作目录
注意,它从当前工作目录(.)开始,用..找到其上一级目录,然后读其目录项,直到该目录项中的i节点编号与工作目录i节点编号相同,这样地就找到了其对应的文件名。按照这种方法,逐层上移,直到遇到根,这样就得到了当前工作目录完整的绝对路径名。
include <unistd.h>
char *getcwd(char +buf,size_t size);
//返回值:若成功,返回buf;若出错,返回NULL
4.23 设备特殊文件
st_dev和st_rdev这两个字段经常引起混淆,在18.9节,我们编写ttyname函数时,需要使用这两个字段。有关规则很简单:
- 每个文件系统所在的存储设备都由其主、次设备号表示。设备号所用的数据类型是基本系统数据类型dev_t。主设备号标识设备驱动程序,有时编码为与其通信的外设板;次设备号标识特定的子设备。一个磁盘驱动器经常包含若干个文件系统。在同一磁盘驱动器上的各文件系统通常具有相同的主设备号,但是次设备号却不同。
- 系统中与每个文件名关联的st_dev值是文件系统的设备号,该文件系统包含了这一文 件名以及与其对应的i节点。
- 我们通常可以使用两个宏:major和minor来访问主、次设备号,大多数实现都定义这两个宏。这就意味着我们无需关心这两个数是如何存放在dev_t对象中的。
- 只有字符特殊文件和块特殊文件才有st_rdev值。此值包含实际设备的设备号。
一般major标识主设备号,minor标识次设备号。
4.24 文件权限访问小结(写不动了,直接上截图吧)
总结 (第四章 文件和目录 完结撒花)
学到这,大家应该对文件系统也有个全面的了解了。文件系统长啥样、怎么创建、怎么去使用、阅读记录都讲明白了,在之后的学习过程中,这些函数的使用将非常重要!内容围绕stat函数,详细介绍了stat结构中的每一个成员。这使我们对UNLX文件和目录的各个属性都有所了解。