我们可以把一个磁盘分成一个或多个分区,每个分区可以包含一种文件系统。
i节点是固定长度的记录项,它包含有关文件的大部分信息。
1.每个i节点中都有一个连接计数,其值是指向该 i节点的目录项数。只有当连接计数减少为0时,才可删除该文件(也就是可以释放该文件占用的数据块)。所以“解除对一个文件的连接”操作并不总是意味着“释放该文件占用的磁盘块”。这也是删除一个目录项的函数被称之为unlink而不是delete的原因。stat结构中st_nlink成员包含了连接计数,其基本系统数据类型是nlink_t。这种连接类型称之为硬连接。POSIX.1常数LINK_MAX指定了一个文件连接数的最大值。
2.另外一种连接类型称之为符号连接(symbolic link) 。对于这种连接,该文件的实际内容(在数据块中)包含的实际是该符号连接所指向的文件的名字。
3.i节点包含了所有与文件有关的信息:文件类型、文件存取许可权位、文件长度和指向该文件所占用的数据块的指针等等。stat结构中的大多数信息都取自i节点。只有两项数据存放在目录项中:文件名和i节点编号数。i节点编号数的数据类型是ino_t。
4.因为目录项中的i节点编号数只能指向同一文件系统中的 i节点,不能使一个目录项指向另一个文件系统的i节点。所以ln(1)命令创建的硬链接(构造一个指向一个现存文件的新目录项),不能跨越文件系统!
5.当在不更改文件系统的情况下为一个文件重新命名(移动)时,该文件的实际内容并未移动,只是构造了一个指向已有 i节点的新目录项,同时删除原来名字对应的老目录项。
例如,将文件 /usr/lib/foo重命名为/usr/foo。若目录/usr/lib和/usr在同一文件系统上,则文件foo的内容无需移动。这也是mv(1)命令的通常操作方式(因此我们会发现,在一个分区的文件系统中移动一个很大的文件,速度也非常地快)。
我们创建一个目录:
mkdir testdir
××任何叶子目录(也就是不包含任何其他目录的目录)的连接计数都为2,一个是命名该目录的目录项..(即父目录),一个是该目录中的. 项。
任何一个文件可以有多个目录项指向其 i节点。创建一个向现存文件连接的方法是使用link函数。函数声明如下:
#include <unistd.h>
int link(const char *existingpath, const char *newpath);
返回:如果成功返回0,如果错误返回-1
此函数创建一个引用现存文件existingpath的新目录项newpath。如若newpath已经存在,则返回出错。另外,创建新目录项以及增加连接计数是个原子操作。只有超级用户进程可以创建指向一个目录的新连接,因为这样做可能在文件系统中成循环。
为了删除一个现存的目录项,可以调用unlink函数。其声明如下:
#include <unistd.h>
int unlink(const char *pathname);
返回:如果成功返回0,如果错误返回-1
此函数删除目录项,并将由pathname所引用的文件的连接计数减1。如果该文件还有其他连接,则仍可通过其他连接存取该文件的数据。如果出错,则不对该文件作任何更改。为了解除对文件的连接,必须对包含该目录项的目录具有写和执行权限。如果对该目录设置了粘滞位(参见前面),则对该目录必须具有写许可权,并且具备下面三个条件之一:
a)拥有该文件。
b)拥有该目录。
c)具有超级用户优先权。
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了,但是因为程序还在打开文件,所以文件内容没有被删除,利用上面这个代码的特性,我们在创建或打开一个临时文件之后,马上调用ulink,这样,当程序崩溃的时候,它所创建的临时文件也不会遗留下来。
利用这个特性,我们也可以恢复一些被删除的文件(前提是文件还有其他进程在使用)。例如我们要恢复一个被删除的文件test_recover,如下:
a)查看当前文件:
$ 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置于后台。
b)删除文件test_recover
$rm test_recover
$ ls
test_recover2
这样,我们已经将文件删除,但是由于less还在运行,所以这个文件虽然我们看不见了,但是less不知道它删除了,less还是可以对文件进行读写的,也就是说这个文件的数据实际还存在于磁盘上。
c)查看删除的文件的信息:
$lsof |grep test_recover
less 22197 quietheart 4r REG 8,8 87 1837925 /home/quietheart/test/lsof_test/test_recover (deleted)
这里,通过lsof我们可以看到被删除的文件的信息。
d)根据删除文件的信息恢复删除的文件:
$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目录下恢复该文件的内容。
如果被unlink的pathname是symbolic link,那么仅仅将symbolic link移除,并没有方法来移除对应的文件。超级用户可以对目录进行unlink,但是一般最好不这么做,应当用rmdir,后面会讲到。
我们也可以用remove解除对一个文件或目录的连接。对于文件,remove的功能与unlink相同。对于目录,remove的功能与rmdir相同。其声明如下:
#include <stdio.h>
int remove(const char *pathname);
返回:如果成功返回0,如果错误返回-1
rename用来重新命名一个文件或者目录,声明如下:
#include <stdio.h>
int rename(const char *oldname, const char *newname);
返回:如果成功返回0,如果错误返回-1
其执行有如下情况:
a.如果oldname是一个文件,不是目录,那么我们会把相应的文件或者符号链接重新命名。这时候,如果newname存在,那么它不能是一个目录的引用。如果newname存在,并且不是一个目录,那么会先将newname删除,然后把oldname重新命名为newname.我们必须具有包含oldname以及newname的目录的写权限。
b.如果oldname是一个目录,那么将会给一个目录重新命名。如果newname存在,它必须是一个目录的引用,并且这个目录必须为空。如果newname存在,并且是一个空目录,那么会被移除,然后将oldname重新命名为newname.另外,当我们重新命名一个目录的时候,newname不能包含oldname作为其路径前缀。例如,我们不能重新命名/usr/foo为/usr/foo/testdir.
c.如果一个oldname或者newname是一个符号链接的引用,那么这些链接会被处理,而不是相应的文件。
d.对于一特殊的情况,若oldname和newname引用相同的文件,那么函数返回成功,并且不会改变任何东西。
硬链接的局限性:
a硬链接和文件必须在同一个文件系统之下。
b只有超级用户才能够创建目录的硬链接。
而符号链接则没有以上限制,符号链接是不能用rmdir来删除的,因为它是一个文件,不是目录。用rm -r 也是只删除了这个链接本身。
当使用以名字引用一个处理文件的函数时,应当知道该函数是否能够处理符号连接。也就是这个函数是否跟随符号连接到达它所连接的文件,然后对被连接的文件进行正确地操作,而不是指向文件的链接本身。
symlink函数创建一个符号连接。声明如下:
#include <unistd.h>
int symlink(const char *actualpath, const char *sympath);
返回:如果成功返回0,如果错误返回-1
该函数创建了一个指向actualpath的新目录项sympath,在创建此符号连接时,并不要求actualpath已经存在。并且,actualpath和sympath不必位于同一文件系统中。
因为open函数跟随符号连接,所以需要有一种方法打开该连接本身,并读取该连接中的名字。readlink函数提供了这种功能。声明如下:
#include <unistd.h>
ssize_t readlink(const char* restrict pathname, char *restrict buf, size_t bufsize);
返回:如果成功返回读取的字节数目;错误返回-1
此函数组合了open, read和close的所有操作。如果成功,则返回读入buf的字节数。在buf中返回的符号连接的内容不以null字符终止。
文件有三种和时间相关的属性:
atime:访问时间。记录最后一次,例如执行exec,或者read,或者创建文件(非截断创建)的时候的时间。
mtime:内容改变时间。记录最后一次文件内容改变的时间。
ctime:状态改变时间。记录最后一次文件索引信息改变的时间,例如:文件名称,大小,链接数目,等一般在stat中的信息的改变。
这里,内容改变时间(st_mtime )和状态改变时间(st_ctime)是不同的:内容改变时间是文件内容最后一次被修改的时间;状态改变时间是该文件的 i节点最后一次被修改的时间。
因为有很多操作,它们只影响到i节点,但并没有更改文件的实际内容:文件的存取许可权、用户ID、连接数等等。而i节点中的所有信息都是与文件的实际内容分开存放的,所以,除了文件数据改变时间以外,还需要状态改变时间。
另外,系统并不保存对一个 i节点的最后一次存取时间,所以access和stat函数并不更改这三个时间中的任一个。下表给出之前讲过的各种函数对这三个时间的作用。
注意,目录是包含目录项(即目录下文件的文件名以及相关i节点编号)的文件,所以对目录项的增加,删除,修改都会影响到目录的3个时间。
我们可以使用utime函数来修改文件的访问时间和内容修改时间。其声明如下:
#include <utime.h>
int utime(const char *pathname, const struct utimbuf *times);
返回:如果成功返回0,如果错误返回-1
这里,utimbuf结构定义如下:
struct utimbuf {
time_t actime; /*access time*/
time_t modtime; /*modification time*/
};
此结构中的两个时间值是日历时间(即1970年1月1日,00:00:00至今的秒数)。此函数的操作以及执行它所要求的优先权取决于参数times是否为NULL。
(a) 如果times是空指针,则存取时间和修改时间两者都设置为当前时间。此操作必须满足下列两条件之一:进程的有效用户ID必须等于该文件的所有者ID;进程对该文件必须具有写许可权。
(b) 如果times非空,则存取时间和修改时间被设置为times所指向的结构中的值。此时,进程的有效用户ID必须等于该文件的所有者ID,或者进程必须是一个超级用户进程。对文件只具有写许可权是不够的。
注意,我们不能对更改状态时间st_ctime指定一个值,当调用utime函数时,此字段被自动更新。
mkdir函数可以创建一个目录,其声明如下:
#include <sys/stst.h>
int mkdir(const char *pathname, mode_t mode);
返回:如果成功返回0,如果错误返回-1
对于目录,要至少设置一个执行权限位,以允许访问该目录中的文件名。
空目录通过函数rmdir被删除。这里空目录表示目录里面只有”.”和”..”目录项。声明如下:
#include <unistd.h>
int rmdir(const char *pathname);
返回:如果成功返回0,如果错误返回-1
如果此调用使目录的连接计数变成0,并且也没有其他进程打开此目录,则释放此目录所占空间。如果在连接计数达到0时,有一个或几个进程打开了此目录,则在此函数返回前删除最后一个连接及. 和.. 项,另外,在此目录中不能再创建新文件。在最后一个进程关闭它之前并不释放此目录(即使某些进程打开该目录,这些进程在此目录下也不能执行其他操作,因为为使rmdir函数成功执行,该目录必须为空)。