APUE读书笔记-04文件和目录(3)
11、 chown
, fchown
,和 lchown
函数
chown
函数允许我们修改文件的 User-ID
或者 Group-ID
。声明如下:
include <unistd.h>
int chown(const char *pathname, uid_t owner, gid_t group);
int fchown(int filedes, uid_t owner, gid_t group);
int lchown(const char *pathname, uid_t owner, gid_t group);
三者返回:如果成功返回0,如果错误返回1(其实值一般为-1)。
除了所引用的文件是符号连接情况以外,这三个函数的操作相类似。参数含义类似前面, chown
和 lchown
修改路径指向文件的属主/组,而 fchown
修改被打开的文件描述符号代表的文件的属主/组。在是符号连接情况下, lchown
更改符号连接本身的所有者,而不是该符号连接所指向的文件。
译者注
原文参考
12、文件的大小
stat
结构的 st_size
成员包含了文件的大小,这个成员仅对正规文件(也就是普通文件),目录,链接文件有意义。对于普通文件,如果大小为0,则表示我们读取文件的时候将会获得一个文件结束标记;对于目录文件,文件大小一般是一个数的整数倍(例如 16
或者 512
),我们后面会讨论目录读取;对于符号链接,文件大小表示链接所指向的文件的文件名称的大小。
例如:
lrwxrwxrwx 1 root 7 Sep 25 07:14 lib -> usr/lib
这里的例子中,符号链接文件 lib
的大小就是其所指向的文件名称 usr/lib
的名称大小(注意:符号链接的大小不包含C语言的用来结束字符串的空字符)。
目前多数 UNIX 系统提供了 st_blksize
和 st_blocks
域。前者表示这个文件进行 I/O
的时候的期望块大小,后者表示实际分配的 512
字节块的数目。前面第3章第8节中我们曾经说过,读取一个文件,设置不同的缓存,消耗的时间有所不同。当我们设置其大小为 st_blksize
的时候,实际就是最优的大小,这时候读取文件消耗的时间最小。后面我们讨论的标准 I/O
库就因为效率的原因,尝试每次都是用 st_blksize
字节的大小进行读写。另外,我们需要注意,不同版本的 UNIX 系统使用的 st_blocks
的单位可能不全是 512
字节块。
文件空洞
在第3章中我们提到一个普通文件可能包括空洞。空洞可以通过定位到超过文件的结尾之后的某个位置,然后进行写,这样的方式来产生。例如:
$ls -l core
-rw-r--r-- 1 sar 8483248 Nov 18 12:18 core
$ du -s core
272 core
这样看到文件的大小是 8M
,但是通过 du
命令看到的大小确说明这个文件所占用的磁盘空间是 272
个 512
字节大小的块( 139
, 264
字节)。通过这个结果,我们显然可以看出来,文件包含空洞。
正如我们第3章中所讲述的,对于没有被写入的文件位置,读取函数返回的数据字节数目为0。如果我们如下执行:
$wc -c core
8483248 core
从这里我们可以看到,普通的 I/O
操作,读取了整个大小的文件。带有 -c
选项的 wc
命令会对文件中的字符(字节)数目进行计数。
如果我们使用 cat
对文件进行拷贝,那么所有的文件空洞将被当作实际的数据0写入生成的文件,如下:
$cat core > core.copy
$ls -l core*
-rw-r--r-- 1 sar 8483248 Nov 18 12:18 core
-rw-rw-r-- 1 sar 8483248 Nov 18 12:27 core.copy
$ du -s core*
272 core
16592 core.copy
通过这里,我们可以看到,新文件实际的占用字节数目是 8,495,104
字节(就是 512x16,592
字节)。此长度与 ls
命令报告的长度( 8483248
)之间的差别是由于文件系统使用了若干块以保存指向实际数据块的指针。
译者注
原文参考
13、文件截断
有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例, open
时候用 O_TRUNC
标志可以做到这一点。
为了截断文件可以调用函数 truncate
和 ftruncate
。声明如下:
#include <unistd.h>
int truncate(const char *pathname, off_t length);
int ftruncate(int filedes, off_t length);
两者返回:如果成功返回0,如果错误返回1(实际值应该为-1)。
函数的参数和前面函数类似,不重复。函数的作用是将指定的存在的文件的长度截短为 length
。如果该文件以前的长度大于 length
,则超过 length
以外的数据就不再能存取。如果以前的长度短于 length
,则其结果依赖系统实现。若某个实现的处理是扩展该文件,则在以前的文件尾端和新的文件尾端之间的数据将读作0 (也就是在文件中创建了空洞)。
译者注
原文参考
14、文件系统
为了解文件链接的概念,需要对UNIX文件系统的概念有所了解。了解到一个 i-node
和一个指向 i-node
的目录项之间的不同之处,是非常有用的。目前有多种UNIX文件系统的实现。这些文件系统都不影响下面的讨论。本节讨论传统的UNIX系统V文件系统。
我们可以把一个磁盘分成一个或多个分区,每个分区可以包含一种文件系统。
磁盘驱动、分区、和文件系统
+------------------+------------------+------------------+
disk drive | partition | partition | partition |
+------------------x------------------x------------------+
/ \
/----------/ \-----------------\
/ \
x-+-+---------------+---------------+---+---------------x
file system | | |cylinder group0|cylinder group1|...|cylinder groupn|
+-+-+---------------x---------------x---+---------------+
| | /-------/ \-----------------\
boot blocks <-+ | / \
super block<-+ x-----------+----+------+------+-------+--------x
|super block| cg |i-node|block |i-nodes| data |
| copy |info| map |bitmap| | blocks |
+-----------+----+------+------x-------x--------+
/ \
/---/ \----\
/ \
x------+------+---+------x
|i-node|i-node|...|i-node|
+------+------+---+------+
i-nodes
是固定长度的记录项,它包括一个文件的大多数信息。
如果我们对 i-node
和一个柱面组数据块部分 ,我们得到如下图示:
柱面组中i-nodes和数据块的细节
+------+--+-----+--+-----+--+---------+-----+-----+--+---------+--+
|i-node|xx| data|xx| data|xx|directory|xxxxx|data |xx|directory|xx|
|array |xx|block|xx|block|xx| block |xxxxx|block|xx| block |xx|
x------x--+^----+--+^----+--+---------+-----+-^---+--+---------+--+
/ \ | second/data | \ / \ \
/ --+------/-- | \ / \ \
/ first|data / \ third data \ / \ \
/ | / ----x-----+-------------X \ ------------\
/ | / / \ v -------\ | \
/ | / / \ +----------------------v | \
x------+------+-+-|-/-/+-+------x |-------------+--------| \ \
|i-node|i-node|X|i-node|X|i-node| |i-node number|filename| \ v
+------+------+-+-^--^-+-+------+ /|-------------+--------| v----------------------+
| | / +----------------------+ |-------------+--------|
| +----------- /---|i-node number|filename|
| / |-------------+--------|
+------------------------------------------- +----------------------+
我们需要注意如下两点:
- 在图中有两个目录项指向同一i节点。每个i节点中都有一个连接计数,其值是指向该 i节点的目录项数。只有当连接计数减少为0时,才可删除该文件(也就是可以释放该文件占用的数据块)。所以“解除对一个文件的连接”操作并不总是意味着“释放该文件占用的磁盘块”。而删除一个目录项的函数被称之为
unlink
而不是delete
的原因。stat
结构中st_nlink
成员饱含了连接计数,其基本系统数据类型是nlink_t
。这种连接类型称之为硬连接。POSIX.1
常数LINK_MAX
指定了一个文件连接数的最大值。 - 另外一种连接类型称之为符号连接(
symbolic link
) 。对于这种连接,该文件的实际内容(在数据块中)包含的实际是该符号连接所指向的文件的名字。 - i节点包含了所有与文件有关的信息:文件类型、文件存取许可权位、文件长度和指向该文件所占用的数据块的指针等等。
stat
结构中的大多数信息都取自i节点。只有两项数据存放在目录项中:文件名和i节点编号数。i节点编号数的数据类型是ino_t
。 - 因为目录项中的i节点编号数只能指向同一文件系统中的 i节点,不能使一个目录项指向另一个文件系统的i节点。所以
ln(1)
命令创建的硬链接(构造一个指向一个现存文件的新目录项),不能跨越文件系统。我们将在下一节讲述link
函数。 - 当在不更改文件系统的情况下为一个文件重新命名(移动)时,该文件的实际内容并未移动,只是构造了一个指向已有 i节点的新目录项,同时删除原来名字对应的老目录项。
例如,将文件 /usr/lib/foo
重命名为 /usr/foo
。若目录 /usr/lib
和 /usr
在同一文件系统上,则文件 foo
的内容无需移动。这也是 mv(1)
命令的通常操作方式(因此我们会发现,在一个分区的文件系统中移动一个很大的文件,速度也非常地快)。
我们说明了普通文件的连接计数的概念,但是对于目录文件的连接计数字段又如何呢?假定我们在工作目录中构造了一个新目录:
$mkdir testdir
下图展示了结果的情况(注意这里我们特别指明了 .
和 ..
):
创建目录testdir之后的柱面组
+------------+-----------+---------------+--------+---------------+-------+
|i-node array| XXXXX |directory block| XXXXXX |directory block| XXXXX |
+------------+-----------+----------^----+--------+----^----------+-------+
/ X-----------\---------+--\ | \ | \
/ / \ data\ block | \|data block\ / \
/ / \ -------\-------+ +-----------X \
| / ---/--| | | \ \
v / / v | | \ \
+------+-+---/--+-+---/--+-+ \ | \ \
|i-node| |i-node| |i-node| | v v \ |
| 0 | | 1267 | | 2549 | | +----+----+ | |
+------+-+---^-^+-+---^-^+-+ |2549| . | | |
| \ | \ /+----+----+ | |
| \ | ------- |1267| .. | v v
| \ | / +----+----+ +------+-------+
| ---+--------- /|1267 | . |
| | / +------+-------+
+--------+------------------------------------- |i-node| .. |
| |number| |
| |------+-------|
| |------+-------|
| /|2549 |testdir|
+-------------------------------------- |------+-------|
+------+-------+
i-node
节点号为 2549
的,其类型域为 directory
,链接计数为2。任何叶子目录(也就是不包含任何其他目录的目录)的连接计数都为2。这个值为2的原因是:包含该目录的目录项中有一个指向该目录的,还有该目录本身中有一个 .
的目录项表示当前目录,也指向本目录。 i-node
号为 1267
的目录,链接数目为3。因为除了前面所指出的它的父目录,以及它本身中的 .
指向它,这个目录的子目录 testdir
中的 ..
(表示该子目录的父目录)也指向了它。也就是说,任何子目录中的 ..
导致其父目录的链接引用计数增加1。
译者注
原文参考
15、 link
, unlink
, remove
,以及 rename
函数
任何一个文件可以有多个目录项指向其 i 节点。创建一个向现存文件连接的方法是使用 link
函数。函数声明如下:
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
返回:如果成功返回0,如果错误返回1(其实际值一般为-1)。
此函数创建一个引用现存文件 existingpath
的新目录项 newpath
。如若 newpath
已经存在,则返回出错。另外,创建新目录项以及增加连接计数是个原子操作。只有超级用户进程可以创建指向一个目录的新连接。因为这样做可能在文件系统中成循环,大多数处理文件系统的公用程序都不能处理这种情况。
为了删除一个现存的目录项,可以调用 unlink
函数。其声明如下:
#include <unistd.h>
int unlink(const char *pathname);
返回:如果成功返回0,如果错误返回1(其实际值一般为-1)。
此函数删除目录项,并将由 pathname
所引用的文件的连接计数减1。如果该文件还有其他连接,则仍可通过其他连接存取该文件的数据。如果出错,则不对该文件作任何更改。为了解除对文件的连接,必须对包含该目录项的目录具有写和执行权限。如果对该目录设置了粘滞位(参见前面),则对该目录必须具有写许可权,并且具备下面三个条件之一:
- 拥有该文件。
- 拥有该目录。
- 具有超级用户优先权。
unlink
一个文件的时候,如果文件 link
数大于0,则不删除,如果 link
数等于0则删除。另外,如果 link
数等于0了,还有进程再打开这个文件,那么内容先不删除(尽管这时候在目录看不见那个目录了),等关闭文件的时候,内核会检查 link
数目为0了才删除。也就是,首先内核检查引用文件的进程计数,如果为0,则再检测链接计数,链接计数也为0了,就删除文件内容。
如下代码,打开一个文件之后再将它 unlink
,
#include "apue.h"
#include <fcntl.h>
int main(void)
{
if (open("tempfile", O_RDWR) < 0)
err_sys("open error");
if (unlink("tempfile") < 0)
err_sys("unlink error");
printf("file unlinked\n");
sleep(15);
printf("done\n");
exit(0);
}
虽然 unlink
了,但是因为程序还在打开文件,所以文件内容没有被删除,利用上面这个代码的特性,当程序崩溃的时候,就不会给系统留下“垃圾”文件(程序的临时文件)。
如果被 unlink
的 pathname
是 symbolic link
,那么仅仅将 symbolic link
移除,并没有方法来移除对应的文件。超级用户可以对目录进行 unlink
,但是一般最好不这么做,应当用 rmdir
,后面会讲到。
我们也可以用 remove
解除对一个文件或目录的连接。对于文件, remove
的功能与 unlink
相同。对于目录, remove
的功能与 rmdir
相同。其声明如下:
#include <stdio.h>
int remove(const char *pathname);
返回:如果成功返回0,如果错误返回1(实际值一般为-1)。
rename
用来重新命名一个文件或者目录,声明如下:
#include <stdio.h>
int rename(const char *oldname, const char *newname);
返回:如果成功返回0,如果错误返回1(实际值一般为-1)。
其执行有如下情况:
-
如果
oldname
是一个文件,不是目录,那么我们会把相应的文件或者符号链接重新命名。这时候,如果
newname
存在,那么它不能是一个目录的引用。如果newname
存在,并且不是一个目录,那么会先将newname
删除,然后把oldname
重新命名为newname
. 我们必须具有包含oldname
以及newname
的目录的写权限。 -
如果
oldname
是一个目录,那么将会给一个目录重新命名。如果
newname
存在,它必须是一个目录的引用,并且这个目录必须为空。如果newname
存在,并且是一个空目录,那么会被移除,然后将oldname
重新命名为newname
. 另外,当我们重新命名一个目录的时候,newname
不能包含具有oldname
前缀的目录。例如,我们不能重新命名/usr/foo
为/usr/foo/testdir
。 -
如果一个
oldname
或者newname
是一个符号链接的引用,那么这些链接会被处理,而不是相应的文件。 -
对于一特殊的情况,若
oldname
和newname
引用相同的文件,那么函数返回成功,并且不会改变任何东西。
译者注
关于文件的恢复
利用 ulink
的特性,我们也可以恢复一些被删除的文件(前提是文件还有其他进程在使用)。例如我们要恢复一个被删除的文件 test_recover
,如下:
1、查看当前文件
$ ls
test_recover test_recover2
$less test_recover
goodfile
it is very good!!
it is used to test how to recover a deleted file with lsof.
这时候,文件还处于没有删除的状态,在这个状态下,我们使用 less
打开了文件,并把 less
置于后台。
2、删除文件 =test_recover =
$rm test_recover
$ ls
test_recover2
这样,我们已经将文件删除,但是由于 less
还在运行,所以这个文件虽然我们看不见了,但是 less
不知道它删除了, less
还是可以对文件进行读写的,也就是说这个文件的数据实际还存在于磁盘上。
3、查看删除的文件的信息
$lsof |grep test_recover
less 22197 quietheart 4r REG 8,8 87 1837925 /home/quietheart/test/lsof_test/test_recover (deleted)
这里,通过 lsof
我们可以看到被删除的文件的信息。
4、根据删除文件的信息恢复删除的文件
$cd /proc/22197/fd
$ ls
0 1 2 3 4
$cat 4
goodfile
it is very good!!
it is used to test how to recover a deleted file with lsof.
$ cat 4 >/home/quietheart/test/lsof_test/test_recover
$ cd /home/quietheart/test/lsof_test/
$ ls
test_recover test_recover2
$ cat test_recover
goodfile
it is very good!!
it is used to test how to recover a deleted file with lsof.
根据前面 lsof
的信息,我们知道,被删除的文件实际就是 less
程序的文件描述符号4,根据此我们确定了要恢复的文件。使用 cat
对文件进行恢复。
总结
对于许多应用程序,尤其是日志文件和数据库,这种恢复删除文件的方法非常有用。
如前面所述,再总结一下,这样可以恢复文件,其原理是:当进程打开了某个文件时,只要该进程保持打开该文件,即使将其删除,它依然存在于磁盘中。当文件删除时,进程并不知道文件已经被删除,它仍然可以向打开该文件时提供给它的文件描述符进行读取和写入。除了该进程之外,这个文件是不可见的,因为已经删除了其相应的目录索引节点。 在 /proc
目录下,其中包含了反映内核和进程树的各种文件。 /proc
目录挂载的是在内存中所映射的一块区域,所以这些文件和目录并不存在于磁盘中,因此当我们对这些文件进行读取和写入时,实际上是在从内存中获取相关信息。大多数与 lsof
相关的信息都存储于以进程的 PID
命名的目录中,即 /proc/1234
中包含的是 PID
为 1234
的进程的信息。每个进程目录中存在着各种文件,它们可以使得应用程序简单地了解进程的内存空间、文件描述符列表、指向磁盘上的文件的符号链接和其他系统信息。 lsof
程序使用该信息和其他关于内核内部状态的信息来产生其输出。所以 lsof
可以显示进程的文件描述符和相关的文件名等信息。也就是我们通过访问进程的文件描述符可以找到该文件的相关信息。当系统中的某个文件被意外地删除了,只要这个时候系统中还有进程正在访问该文件,那么我们就可以通过 lsof
从 /proc
目录下恢复该文件的内容。