LinuxLinux学习之路

APUE读书笔记-04文件和目录(3)

2020-04-08  本文已影响0人  QuietHeart

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)。

除了所引用的文件是符号连接情况以外,这三个函数的操作相类似。参数含义类似前面, chownlchown 修改路径指向文件的属主/组,而 fchown 修改被打开的文件描述符号代表的文件的属主/组。在是符号连接情况下, lchown 更改符号连接本身的所有者,而不是该符号连接所指向的文件。

译者注

原文参考

参考: APUE2/ch04lev1sec11.html

12、文件的大小

stat 结构的 st_size 成员包含了文件的大小,这个成员仅对正规文件(也就是普通文件),目录,链接文件有意义。对于普通文件,如果大小为0,则表示我们读取文件的时候将会获得一个文件结束标记;对于目录文件,文件大小一般是一个数的整数倍(例如 16 或者 512 ),我们后面会讨论目录读取;对于符号链接,文件大小表示链接所指向的文件的文件名称的大小。

例如:

lrwxrwxrwx 1 root           7 Sep 25 07:14 lib -> usr/lib

这里的例子中,符号链接文件 lib 的大小就是其所指向的文件名称 usr/lib 的名称大小(注意:符号链接的大小不包含C语言的用来结束字符串的空字符)。

目前多数 UNIX 系统提供了 st_blksizest_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 命令看到的大小确说明这个文件所占用的磁盘空间是 272512 字节大小的块( 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 )之间的差别是由于文件系统使用了若干块以保存指向实际数据块的指针。

译者注

原文参考

参考: APUE2/ch04lev1sec12.html

13、文件截断

有时我们需要在文件尾端处截去一些数据以缩短文件。将一个文件的长度截断为0是一个特例, open 时候用 O_TRUNC 标志可以做到这一点。

为了截断文件可以调用函数 truncateftruncate 。声明如下:

#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 (也就是在文件中创建了空洞)。

译者注

原文参考

参考: APUE2/ch04lev1sec13.html

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|
                  |                                           /    |-------------+--------|
                  +-------------------------------------------     +----------------------+

我们需要注意如下两点:

例如,将文件 /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。

译者注

原文参考

参考: APUE2/ch04lev1sec14.html

15、 linkunlinkremove ,以及 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。如果该文件还有其他连接,则仍可通过其他连接存取该文件的数据。如果出错,则不对该文件作任何更改。为了解除对文件的连接,必须对包含该目录项的目录具有写和执行权限。如果对该目录设置了粘滞位(参见前面),则对该目录必须具有写许可权,并且具备下面三个条件之一:

  1. 拥有该文件。
  2. 拥有该目录。
  3. 具有超级用户优先权。

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 了,但是因为程序还在打开文件,所以文件内容没有被删除,利用上面这个代码的特性,当程序崩溃的时候,就不会给系统留下“垃圾”文件(程序的临时文件)。

如果被 unlinkpathnamesymbolic 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)。

其执行有如下情况:

  1. 如果 oldname 是一个文件,不是目录,那么我们会把相应的文件或者符号链接重新命名。

    这时候,如果 newname 存在,那么它不能是一个目录的引用。如果 newname 存在,并且不是一个目录,那么会先将 newname 删除,然后把 oldname 重新命名为 newname . 我们必须具有包含 oldname 以及 newname 的目录的写权限。

  2. 如果 oldname 是一个目录,那么将会给一个目录重新命名。

    如果 newname 存在,它必须是一个目录的引用,并且这个目录必须为空。如果 newname 存在,并且是一个空目录,那么会被移除,然后将 oldname 重新命名为 newname . 另外,当我们重新命名一个目录的时候, newname 不能包含具有 oldname 前缀的目录。例如,我们不能重新命名 /usr/foo/usr/foo/testdir

  3. 如果一个 oldname 或者 newname 是一个符号链接的引用,那么这些链接会被处理,而不是相应的文件。

  4. 对于一特殊的情况,若 oldnamenewname 引用相同的文件,那么函数返回成功,并且不会改变任何东西。

译者注

关于文件的恢复

利用 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 中包含的是 PID1234 的进程的信息。每个进程目录中存在着各种文件,它们可以使得应用程序简单地了解进程的内存空间、文件描述符列表、指向磁盘上的文件的符号链接和其他系统信息。 lsof 程序使用该信息和其他关于内核内部状态的信息来产生其输出。所以 lsof 可以显示进程的文件描述符和相关的文件名等信息。也就是我们通过访问进程的文件描述符可以找到该文件的相关信息。当系统中的某个文件被意外地删除了,只要这个时候系统中还有进程正在访问该文件,那么我们就可以通过 lsof/proc 目录下恢复该文件的内容。

原文参考

参考: APUE2/ch04lev1sec15.html

上一篇下一篇

猜你喜欢

热点阅读