本篇索引:
1、引言
2、文件类型
3、获取文件属性的函数,stat、fstat、lstat
4、超级用户(root用户)和普通用户
5、进程与用户ID
6、文件权限的检查
7、新创建的的文件和目录的所有权
8、access函数
9、umask函数
10、chmod、fchmod函数
11、粘住位
12、chown,fchown,lchown函数
13、文件长度
14、文件截断函数
15、文件系统概述
16、link,unlink,remove,rename函数
17、符号连接
18、与符号相关的函数,symlink和readlink函数
19、文件的时间
20、mkdir函数和rmdir函数
21、打开目录,读目录等函数
22、chdir、和getcwd函数
23、自己简单实现ls命令
1、引言
上一章通过学习文件io,知道了如何打开文件、对文件进行读写数据等的操作,那么我们这一章将换一个角度,专门围绕文件属性进行相关的讨论。
2、文件类型
之前说过,在Linux下一切皆是文件。应用层看待底层机制时,一切皆以文件的眼光看待,但是底层的机制之间的毕竟是有所不同,根据这些不同,文件也被分为如下几种。
2.1、普通文件(regular file:符号-),其又分为如下两种
a)、文本文件(ascci二进制文件)
b)、二进制文件(纯二进制文件)
对linux内核而言,这两种文件并无区别,具体如何解释均有应用程序说了算。
2.2、目录文件(director file:符号d):一种特殊的文件,用以包含其它文件的文件名字和指向这些
文件对应i节点的节点编号,目录允许读,那么用户就可以读目录文件,但是只有内核可以写目录 文件。
2.3、字符特殊文件(character special file:符号c):对应字符设备。
2.4、块特殊文件(block special file:符号b):对应块设备(如磁盘等)。
2.5、FIFO(符号p):有名管道文件,用于进程间通信,是一种纯软机制。
2.6、套接口(socket:符号s):用于实现跨机进程间的网络通信,当然也可用于实现本地(本机)
进程间的通信。
2.7、符号连接(symbolic link:符号l):用以指向另外一个文件,类似于windos界面下的快捷图标。
所有的这些文件中,普通文件数量最多,最常见,其次是目录文件。
3、获取文件属性的函数,stat、fstat、lstat
3.1、函数原型
#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
3.2、函数功能
获取文件的属性信息,文件的属性信息存在了磁盘上该文件对应的inode(i结点)结点中,但
是open打开文件时又会从磁盘i节点中复制一份到内存v节点中i节点结构中,这三个函数还
是有一些区别的。
a)、stat:利用文件路径名从磁盘上的i节点中读取文件属性到buf结构体中。
b)、fstat:前提是文件已经打开,利用文件描述符直接从v结点中的i结点结构中读出文件的属
性,该函数直接从内存中读,但是stat直接从磁盘读取。
c)、lstat:如果操作的文件是符号连接文件时,直接读取的是符号连接文件的属性信息,而stat
和fstat读的是符号连接文件指向文件的属性。所以我们说stat和fstat是一个符号跟随的函 数,而lstat不是。有关符号跟随的问题后面将再次讲解。
3.4、参数说明
a)、const char *path:文件路径名。
b)、int fd:文件描述符。
c)、struct stat *buf:存放读出的文件属性信息。
3.4.1、struct stat *buf结构体说明
struct stat {
dev_t st_dev; /* 块设备号 */
ino_t st_ino; /* inode结点号 */
mode_t st_mode; /* 文件类型和文件权限*/
nlink_t st_nlink; /* 链接数 */
uid_t st_uid; /* 文件所属用户ID*/
gid_t st_gid; /* 文件所属组ID */
dev_t st_rdev; /* 字符设备ID */
off_t st_size; /* 文件大小 */
blksize_t st_blksize; /* 系统每次按块Io操作时,块的大小(一般是512或1024) */
blkcnt_t st_blocks; /* 块的索引号 */
time_t st_atime; /* 最后一次访问时间,如read*/
time_t st_mtime; /* 最后一次修改时间 */
time_t st_ctime; /* 最后一次状态修改的时间,如权限,所有者的修改 */
};
以上这些结构体成员专门用来存放文件的相关属性,囊括了我们讲的7种文件,但是每种类型的文件都不会全用上这些成员项。比如说块设备文件用了块设备ID st_dev后,就不会使用字符设备ID st_rdev,又如文件的大小只有普通文件和目录才有,其它文件是没有的。
3.4.2、st_uid、st_gid
st_uid:文件所属用户ID
st_gid:文件所属组ID
3.4.3、st_mode
st_mode其实是unsigned short(被typedef为了mode_t)的类型,包含了文件类型、设置位和文件权限,看下图:
这些宏使用起来并不是很方便,我们知道了这些宏实现的原理后,大可自己按照这个方法操作,不必一定要使用这些宏。
如果大家忘记了这些宏的话,可在linux下执行man 2 stat,手册中对这些宏有详细的说明。
3.4.4、ls -al看到的文件权限
7种文件决大部分属性是相同的,但是每种不同的文件都有自己独特的属性,这些属性都是stat函数从磁盘中读到了struct stat结构体中,ls -al的目的就是把struct stat结构体描述共有属性显示出来,我们下面以普通文件为例进行说明。
我们执行ls -al main
3.5、返回值
调用成功,返回0,失败返回-1,errno被设置。
3.6、测试用例
3.6.1、stat和lstat函数
事先在当前路径下touch一个file的普通文件,然后调用stat函数获取文件属性。
void err_fun(const char *file, int line, char *oper, int err_no) { fprintf(stderr, "%s %d, %s is fail: %s\n", file, line, oper, strerror(err_no)); exit(-1); } int main(void) { int ret = -1; struct stat buf = {0}; ret = stat("./file", &buf);// ret = lstat("./file", &buf); if(ret < 0) err_fun(__FILE__, __LINE__, "fstat", errno); /* 打印出结构体的各项信息请自己实现 */ return 0; }
3.6.2、fstat函数
void err_fun(const char *file, int line, char *oper, int err_no){ fprintf(stderr, "%s %d, %s is fail: %s\n", file, line, oper, strerror(err_no)); exit(-1); } int main(void) { int ret = -1, fd = -1; struct stat buf = {0}; fd = open("./file", O_RDWR); if(fd < 0) err_fun(__FILE__, __LINE__, "open", errno); ret = fstat(fd, &buf); if(fd < 0) err_fun(__FILE__, __LINE__, "fstat", errno); return 0; }
3.6.3、ls命令的实现
Linux下的各个命令就是一个可执行文件,例如ls命令就是一个可执行文件,存放在了/bin目录下,我们执行ls -al /bin/ls,显示如下:
-rwxr-xr-x. 1 root root 118580 Dec 8 2011 /bin/ls
我然后在执行file /bin/ls,查看文件类型,如下:
/bin/ls: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.18, stripped
ls命令实际上就是调用stat等系统调用的函数读取文件属性并显示出来。
3.7、注意点
a)、注意这三个函数的区别
b)、要清楚st_mode含义和它的一些宏的用法
4、超级用户(root用户)和普通用户
a)、root用户对 / 下所有的文件有读写权限,而我们登陆的linux系统软件的内核代码和一些重
要文件也放在了root下,所以我们并不主张用root权限操作,这可能会修改或删除某些重要文
件导致系统崩溃。
b)、当普通用户登陆成功后,会自动进入家目录下的主目录下(/home/xxxx), 主目录路径记
录在了/etc/passwd中的用户信息中。以我的普通用户linux为例,ls /home/ 看下,结果如下:
linux share smb
家目录/home/存放的是主目录,每个主目录就是每个注册过的用户的合法操作目录,该普通用户在自己的主目录下拥有对所有文件操作的权利。当前普通用户是无法cd进入其它普通用户的主目录,目的是防止你修改或删除其它普通用户私有的文件(添加组ID除外)。
普通用户对/下的文件只有读权限,而没有写权限,目的是为了防止普通用户修改或删除超级用户的/目录下的文件。
在linux用户下试图进入share用户的主目录share是无法成功的,这就防止了linux这个普通用户修改或删除普通用户,同样cd smb也是不行的。但是允许普通用户cd /,只是对于普通用户来说只能读/下的文件,但是无法修改和删除/目录下文件。
5、进程与用户ID
5.1、与一个进程相关的用户ID
不管是执行vi、ls、pwd等命令,还是./a.out执行自己的可执行文件,目的都是为了运行程序,那么与一个进程相关的用户ID都有哪些呢?
1)、实际ID
•实际用户ID
•实际组ID
2)、有效ID
•有效用户ID
•有效组ID
3)、保存ID
•保存有效用户ID
•保存有效组ID
5.2、三种类型ID的区别
5.2.1、实际ID
一般情况下,如果未设置添加组ID的话,用户自己跟自己一组,自己亲自担任组长,用户ID就是自己的组ID。
vi 程序运行时,当前用户是linux的话,进程的实际ID就是linux,ID是我们登录时从/etc/passwd中读出的。即便是我们现在将当前用户切换到了超级用户或其它的普通用户,但是之前运行进程的实际用户ID是不会发生改变的。
实际ID就是看,我们运行该程序的用户的实际用户ID和实际组ID,这个ID取自口令文件/etc/passwd。
5.2.2、有效ID
1)、正常情况
有效组ID由exec函数(保存),正常在没有对可执行文件设置用户ID和设置组ID的情况下,exec保存的有效ID等于实际ID。
2)、特殊情况(设置了用户ID和设置组ID)
初始时exec函数保存有效ID等于实际ID
•如果对可执行文件设置了用户ID的话,exec函数保存的有效用户ID就被修改为可执行
文件的所属用户ID。
•如果对可执行文件设置了组ID的话,exec函数中保存的有效组ID被修改为可执行文件 的所属组ID。
•如果两个设置位都被设置了的话,exec函数保存的有效用户ID和有效组ID都会被修改。
3)、有效ID专门用于文件的权限检查。
5.2.3、保存ID
运行程序时,将exec函数中保存的有效ID保存一个副本。
5.2.4、设置ID
设置文件的用户ID和设置文件的组ID,这在前面已经提到过,目的就是想让进程的实际ID和有效ID发生分离,因为正常情况下,有效ID就是实际ID的复制,但是设置了用户ID和组ID后,exec函数保存的有效用户ID和有效组ID就会被修改为文件的所属用户ID和所属组ID。
密码是用户信息的一部分,它存在了口令文件中,但是用户信息的重要性导致它是不可以被随意更改的,所以它是一个属于超级用户的文件,所以在一般情况下,普通用户是没有办法对其进行修改的,ls -al /etc/passwd,结果如下:
-rw-r--r--. 1 root root 2244 Jan 12 2012 /etc/passwd
看出这个文件的所属用户和所属组都是root,除root外的其它用户对该文件只能读,每个普通用户都有修改自己用户密码的权利,我们常在普通用户下用passwd这个命令进行修改,修改密码就涉及/etc/passwd文件的修改,这就需要用到设置位。
passwd这个命令是一个可执行文件,我们执行这个命令就是为了运行这个程序。实际上shell脚本的作用就是为了逻辑控制shell命令的执行,而每个shell命令其实就是一个可执行文件,每个命令运行起来就是一个程序。
执行命令ls /usr/bin/passwd -al
-rwsr-xr-x. 1 root root 25980 Feb 22 2012 /usr/bin/passwd
这个命令(可执行文件),对于其它普通用户来说只能执行而不能读写,但是被做了用户设置位的设置,所以执行这个程序后,进程的有效用户ID被设置为了root,当它去改写/etc/passwd文件时,这个文件的所属用户ID等于进程的有效用户ID,passwd命令能修改/etc/passwd文件。
6、文件权限的检查
前面多多少少已经讲到了文件权限的检查,本小节将集中讨论文件权限的检查。
文件权限的检查实际上分为两部分:
•文件所属用户ID和所属组ID检查
•文件读写执行权限检查
6.1、文件权限检查的步骤描述
如果是root用户,对文件有无条件的读写权限,但是对于执行权限不是,如果不允许执行,即 便是超级用户也没有办法执行。
a)、检查对文件进行操作的进程的有效用户ID是否等于文件的所属用户ID
•相等:检查文件所属用户对应的存取权限是否允许进程对它的相应存取操作,允许则操作成 功,不允许则相应的操作函数出错返回。
•不相等:执行下一步b
b)、检查对文件进行操作的进程的有效组ID是否等于文件的所属组ID
•相等:检查文件所属组对应的存取权限是否允许进程对它的相应存取操作,允许则操作成功,
不允许则相应的操作函数出错返回。
•不相等:执行下一步c
c)、如果前面的a、b两步都不成立,则直接检查其它用户对应的存取权限是否允许相应的文件存取
操作,允许则操作成功,不允许则相应的文件操作函数出错返回。
以上三步是有优先顺序的,一定是先进行a步的检查,a步不成立才进入b步,b不成立才进入c步
6.2、文件权限检查举例
6.2.1、例子1,vi 命令
阅读本文的同学完全可按照如下步骤操作,我们这里以root用户和普通用户进行说明。
1)、先切换到root用户下
2)、touch file
3)、ls -al file 结果如下
-rw-r--r--. 1 root root 0 Apr 29 11:43 file
文件所属用户可读可写,文件所属组只能读,其它用户只能读
4)、回到普通用户
5)、vi file
vi 命令运行起来后就是一个进程,它去操作file这个文件时也要进行文件权限检查,在没有做设置位设置的情况下(可执行文件vi并未做设置位),vi运行起来后,这个进程的有效用户ID等于实际用户ID,有效组ID等于实际组ID,但都是普通用户ID linux。
a)、检查有效用户ID(linux)是否等文件的所属用户ID(root),结果肯定是不相等
b)、检查有效组ID(linux)是否等文件的所属组ID(root),结果肯定也是不相等
c)、但是其他用户的权限只允许读
6)、我们尝试向file里面写,但是写完后在退出保存时,提示该文件是只读的,不能被写,虽然 加!强制倒是可以,强制退出保存是另一回事。
6.2.2例子2,open函数
这个例子是我们最常见的情况,我们知道open函数在打开文件时,会进行文件权限的检查,我们来看看是如何进行。首先在普通用户下执行touch file。
int main(void){ int fd = -1; fd = open("./file", O_RDONLY); if(fd < 0){ perror("open file is fail"); exit(-1); } }
我们gcc a.c生成可执行文件a.out,当我们运行这个程序的时候,涉及到两个文件权限的检查,第一个就是./a.out时,会对a.out做文件权限检查,当a.out运行打开file文件时,会对file进行相关的文件权限检查。我们一步一步来看下是如何进行。
1)、./a.out 时的文件权限检查
a.out作为一个文件,它也是被别的进程启动后才运行起来的,这里可能涉及后面的知识,我们这里可以先了解,过程如下。
我们ls -al a.out 结果如下
-rwxrwxr-x. 1 linux linux 4885 Apr 29 12:13 a.out //没有做设置位
正常情况下,这里子进程的实际ID和有效ID全部都为linux。
对a.out的权限检查步骤如下:
a)、检查子进程的有效用户ID是否等于a.out这个文件的所属用户ID,显然这里是等于的, 然后所属用户的用户对应的RWX是允许执行的,所以a.out能够正常执行,因为并未做设 置位,所以a.out进程的有效ID不会被修改。
2)、a.out对file文件操作权限的检查
我们首先 ls -al file,结果如下:
-rw-rw-r--. 1 linux linux 12 Apr 29 11:57 file
我们从上面的一步,已经知道运行起来的a.out进程的实际ID和有效ID都是linux,权限检 查的步骤如下:
a)、查看a.out进程的有效用户ID是否等于file文件的所属用户ID,显然是相等的,并且所 属用户对应的rwx权限允许读写,所以open函数打开成功。
6.2.3、例子3,设置位举例
•未做设置位
1)、切换到root用户,touch file 并ls -al file
-rw-r--r--. 1 root root 0 Apr 29 17:58 file
2)、对文件操作的程序还是6.2.2的a.c,打开的方式任然是O_RDONLY打开,gcc a.c
ls -al a.out 结果如下
-rwxrwxr-x. 1 linux linux 5213 Apr 29 18:36 a.out
3)、./a.out运行起来后,进程的有效用户ID和有效组ID都是linux,操作file文件时,权 限检查步骤如下
a)、a.out进程的有效用户ID不等于文件file的所属用户ID
b)、a.out进程的有效组ID也不等于文件file的所属组ID
c)、其它用户对应的读写执行r--允许读,所以a.out以只读打开成功,不会报错
•做设置位
将上面的file的权限改为如下:
-rw-r-----. 1 root root 0 Apr 29 18:36 file
这时我们要是在运行a.out,发现open函数会报Permission denied的错误,这是因为其它用
户的读写执行权限全部---,所以最终权限检查失败,那么在这样的情况下我们就需要对a.out
做设置位。
情况1、设置所属用户设置位
a)、切换到root用户
b)、chown root a.out 将a.out的所属用户修改为root
c)、chmod u+s a.out 设置a.out的所属用户设置位
d)、返回到普通用户,并且ls -al a.out,结果如下
-rwsrwxr-x. 1 root linux 4853 Apr 29 20:20 a.out
所属用户对应的执行为变为了s
由于做了所属用户设置位的设置,./a.out时,子进程从父进程继承过来的有效用户IDlinux会被exec函数在加载a.out函数时,修改为root,但是有效组ID还是继承过来的linux ID。
当a.out打开file文件时,权限检查步骤如下:
a)、检查进程的有效用户ID是否等于文件的file的所属用户ID,因为锁了设置位a.out进程 的有效用户ID就不再是linux,而是root,所以显然是相等的,并且对应的读写权限还 允许它读,所以打开成功。
情况2、设置所属组设置位
e)、切换到root用户
f)、chown :lniux a.out 将a.out的所属组修改为root
g)、chmod g+s a.out 设置a.out的所属组设置位
h)、返回到普通用户,并且ls -al a.out,结果如下
-rwxrwsr-x. 1 linux root 4853 Apr 29 20:20 a.out
所属组对应的执行位变为了s
由于做了所属组设置位的设置,./a.out时,子进程从父进程继承过来的有效组ID linux会被exec函数在加载a.out函数时,修改为root,但是有效用户ID还是继承过来的linux ID。
当a.out打开file文件时,权限检查步骤如下:
a)、检查进程的有效用户ID是否等于文件的file的所属用户ID,显然进程的有效用户ID 为linux,file文件的所属用户为root,不相等。
b)、检查进程的有效组ID是否等于文件的file的所属组ID,因为设置了设置位,a.out进程
的有效组ID就不再是linux,而是root,而file的所属组ID也是root,所以显然是相等
的,并且对应的读写执行权限还允许它读,所以打开成功。
情况3、都设置
我们当然可以将a.out的所属用户和所属组都进行设置位设置,但是实际上起作用的就一个,我们没有必要全都设置,具体看哪一个起作用,需要具体分析权限的检查,前面讲的passwd命令的设置位和上面的例子讲的设置位是一回事。
6.3、目录权限的检查
7种类型的文件都是有文件权限的,并且权限都是由st_mode来说明,所以目录也不例外。目录文件的作用是用来管理其它文件的,当我们想访问其它文件时就必须要能够通过目录,即便默认的的是当前路径,想要访问当前路径下的文件时,对于当前路径的目录必须有相应的访问权限。
对于目录来说:
1、读权限:允许读取目录的目录项列表。
例如:
mkdir kk //创建一个新目录kk
chmod 375 kk //目录kk的权限变为了d-wxrwxr-x. 2 linux linux 4096 Apr 30 12:05 kk
cd kk //进入目录kk
当我们在kk目录下面执行ls命令时,对kk的权限检查步骤如下:
a)、检查ls进程的有效用户ID linux是否等于目录文件kk的所属用户ID linux,显然是相等,所以ls按照-wx操作,但是ls命令调用的是stat函数需要读当前目录kk目录项列表,获取目录项名字,但目录kk所属用户对应的权限-wx并不允许r操作,所以当我们试图ls时,会提示ls: cannot open directory .: Permission denied,ls失败。
2、写权限:只有内核才有写权限,创建了一个新目录,内核需要写目录文件的目录项列表。
例如:
mkdir kk //创建一个新目录kk
chmod 375 kk //目录kk的权限变为了dr-xrwxr-x. 2 linux linux 4096 Apr 30 12:05 kk
cd kk //进入目录kk
当我们在kk目录下面执行mkdir命令,其进行文件权限检查时,检查步骤如下:
a)、检查mkdir进程的有效用户ID linux是否等于目录文件kk的所属用户ID linux,显然是相等,所以mkdir dir按照r-x操作。创建出新的目录的名字将被添加到kk目录的目录项列表中,但目录kk所属用户对用的权限r-x并不允许w操作,所以当我们试图mkdir dir时,会提示mkdir: cannot create directory `dir‘: Permission denied,显然操作失败。
3、执行权限:穿过目录时用到,比如希望打开 ./dir_1/dir_2/file,除了对file有相应的存取权限外,
对于目录./、dir_1和dir_2还必须有相应的可执行权限(x)。
例如:
mkdir kk //创建一个新目录kk
chmod 375 kk //目录kk的权限变为了drw-rwxr-x. 2 linux linux 4096 Apr 30 12:05 kk
cd kk //进入目录kk
试图cd kk进入kk目录时,其进行文件权限检查时,检查步骤如下:
a)、检查cd进程的有效用户ID linux是否等于目录kk的所属用户ID linux,显然是相等,所以cd kk按照rw-的要求操作,目录的x位又可称为穿过目录许可权位,可是目录kk所属用户对用的权限并不允许x操作,所以当试图cd kk时,会提示bash: cd: kk: Permission denied,显然cd kk失败。
6.4、对文件权限的总结
1)、root用户拥有超级权限,即便权限是--- --- ---全都不允许读写,但是超级用户能够有无条件 的对其进行读写,这一点普通用户是做不到的。
但是对于执行位来说,所属用户、所属组、其它用户对应的权限中,至少有一个是允许可执行,如果全部都不许执行的话,root是没办法无条件执行,对于可执行位例外。
2)、对于设置位要清楚一点,只有对于可执行文件做设置位才有意义,因为只有可执行文件才 能在执行后变成进程,才会涉及到进程有用户ID有效组ID。对于非可执行文件进行设置位的设 置是没有意义的,但是这里目录除外,目录也会做设置位的设置
3)、普通用户和超级用户之间存在着权限区别,学到现在我们应该清楚,普通用户和超级用 户的权限的区分是通过文件权限的区分来实现的,普通用户之间是不能够相互访问对方的文件 (添加 组ID除外),具体的原因我们现在就能说清楚了。以我的ubuntu为例。
ls /home/ 结果如下:
drwx------. 39 linux linux 4096 Apr 30 14:46 linux
drwx------. 25 smb smb 4096 Feb 18 15:02 smb
Linux是普通用户linux的主目录名称,smb是普通用户smb的主目录,从这两个的目录的文件权限可以看出,除了用户自己和root超级用户意外,其它用户根本无法访问,所以普通用户之间和普通用户与超级用户之间的权限区别就是通过文件权限的区分来实现的。
7、新创建的的文件和目录的所有权
7.1、对于新创建的文件和目录所有权遵循如下规则
1)、新创建的文件(包括目录在内)所属用户ID设置为进程的有效用户ID,
2)、新文件的有效组ID按如下规则:
a)、如果该新文件所在目录的所属组ID未做设置,新文件的所属组ID为进程的有效组ID
b)、如果该新文件所在目录的所属组ID做了设置,新文件的所属组ID为该文件所在目录的所属组ID
7.2、举例
1)、例1
正常情况下vi或touch file,mkidr kk,试想file和kk的所属用户ID和所属组ID是多少呢?
2)、例2
a)、我们自己写一个程序,调用open函数创建一个新文件file,然后我们对生成a.out所属 用户改为root,并且做所属用户的设置位,新文件file的所属用户ID是什么
b)、将a.out所在目录的所属组ID修改为root,并作组设置位,那么这时新创建的文件file 的所属组ID又什么呢?
8、access函数
open函数打开已存在文件时会进行文件权限检查,用的是进程的有效用户ID和有效组ID,但是有的时候当可执行文件被做了设置为以后,往往进程的实际ID和有效ID不再相同,那么我们可能就像验证下实际ID对于文件是否具有相应的存取权限呢,access函数就能够满足这样的功能。
8.1函数原型和所需头文件
#include <unistd.h>
int access(const char *pathname, int mode);
8.2、函数功能:按照实际ID对文件进行相应的存取权限测试
8.3、函数参数
•const char *pathname:文件路径名
•int mode:文件擦开放式说明,mode与如下选择
F_OK:测试文件是否存在
R_OK:测试文件是否允许读
W_OK:测试文件是否允许写
X_OK:测试文件是否允许执行
以上多个选项可用|组合。
8.4、函数返回值:成功返回0,失败返回-1,errno被设置
8.5、测试用例
1)、切换到root用户,然后touch file,ls file -al
-rw-r--r--. 1 root root 0 Apr 30 14:46 file
2)、回到普通用户,编译下面的程序,得到可执行文件a.out
int main(void) { int fd = -1; fd = access("file", R_OK|W_OK); if(fd < 0) perror("access file is fail\n"); else printf("access file sucess\n"); fd = open("./file", O_RDWR); if(fd < 0) perror("open file is fail\n"); else printf("open file sucess\n"); return 0; }
3)、执行chown root a.out,接着再执行chmod u+s a.out
4)、切回到普通用户,然后ls -al a.out,可以看到我们做了所属用户设置位的设置
-rwsrwxr-x. 1 root linux 5181 Apr 30 14:48 a.out
5)、执行./a.out时涉及到的一系列权限检
•对a.out做文件权限检查
首先某个父进程产生一个子进程,子进程的实际ID和有效ID均继承于父进程,均为我的普通用户linux,然后子进程再进一步运行a.out,运行a.out是权限检查如下:
a)、进程的有效用户ID linux肯定不等于可执行文件a.out的所属用户ID root,执行下 一步。
b)、进程的有效组ID linux肯定等于可执行文件a.out的所属组ID linux,并且对应的 权限允许执行,所以a.out肯定能被成功运行。
但是由于所属用户做了设置位设置,所以a.out进程的有效用户ID从原来的继承过来的linux变为了root,不过有效组ID任然是linux。
•access函数用实际ID对file文件做文件权限检查
由于a.out的实际ID(实际用户ID和实际组ID)都是linux,access检查时只能按照其它用户对应的权限进行检查,而其它用户对应的权限不允许写,所以access对file文件进行写权限测试失败。
•open函数用有效ID对file文件做文件权限检查
从前面知道,a.out进程的有效用户ID变为了root,等于file文件的所述用户ID,并且对应的读写执行权限是允许读和写,所以open file文件成功
所以从这个例子我们看到一个奇怪现象,同样对file进程权限的检查,一个函数的权限测试成功,而另一个失败了,究其原因就是因为一个是用实际ID测试的,另一个是用有效ID测试的,并且通常在做了设置位后,一般来说实际ID和有效ID又会不同,所以才会导致这个现象。
9、umask函数
9.1函数原型和所需头文件
#include <sys/types.h>
#include <sys/stat.h>
mode_t umask(mode_t mask);
9.2、函数功能:修改文件权限掩码
9.3、函数参数
•mode_t mask:新的文件权限掩码
9.4、函数返回值:此函数调用永远都会成功返回,返回的是修改前的文件权限掩码
9.5、测试用例
int main(void) { int fd = -1; fd = open("file", O_RDONLY|O_CREAT, 0777); return 0; }
本意想创建出777(rwxrwxrwx)权限的的file文件,但是实际上这个新创建的file文件的取权限如下,ls -al file:
-rwxrwxr-x. 1 linux linux 0 Apr 30 20:57 file
实际得到的权限是0775(rwxrwxr-x),原因就是权限掩码对指定的权限做了修改,默认的权限掩码是0002(八进制),我们最终得到的文件权限是按照如下规则得到的。
文件最终权限 = (~(文件掩码))& 指定权限
0775 = (~(0002))&(0777)
所以0775是这么得到,当然这个权限掩码是可以被的,比如将其修改为0,
int main(void) { int fd = -1; printf("%#o\n", umask(0000)); fd = open("file", O_RDONLY|O_CREAT, 0777); return 0; }
文件权限掩码修被改为了0000,并且返回修改前的文件权限掩码并打印了出来。然后我们再来看看新创建的file文件的权限如下,ls -al file:
-rwxrwxrwx. 1 linux linux 0 Apr 30 20:57 file
达到了我们的目的
9.6、注意点
1)、我们在shell终端执行umask命令,看到默认的文件权限掩码是0002,而我们的程序默认的 0002就是继承自shell终端的0002。每一个进程都有自己的文件权限掩码,所以上面我们修改程序里的权限掩码,并不会修改掉shell的,当我们吧程序的掩码修改为0000,但shell终端(也 是一个运行的进程)任然是0002,不会有任何的改变。
2)、默认的文件权限掩码为什么是0002呢?原因是为了防止新创建出来的文件被用户将修改的 可能,所以将新创建的文件的其它用户对应权限中的w位设定为-,起到保护的作用。一般情况 下,我们不主张将默认0002权限修改为0000,因为这不利于文件的保护。
10、chmod、fchmod函数
10.1函数原型和所需头文件
#include <sys/stat.h>
int chmod(const char *path, mode_t mode);
int fchmod(int fd, mode_t mode);
10.2、函数功能:修改文件权限,一个是用文件路径操作,一个是用文件描述符操作
10.3、函数参数
•const char *path:文件路径
•int fd:文件描述符
10.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
10.5、测试用例:略
10.6、注意
1)、我们常见的chmod命令就是这两函数的一个调用界面
2)、chmod命令、chmod函数和fchmod函数想要被合法执行需满足下面的条件
a)、超级用户,无条件执行
b)、如果在普通用户下,进程的有效用户ID等于文件所属用户ID
比如file文件,它的权限如下
-rw-rw-r--. 1 root linux 0 Apr 30 21:33 file
当我们在普通用户执行chmod函数或chmod命令时,因为进程的有效用户ID是普通用户,而不是root,所以在在普通用户相面执行chmod命令或函数失败,报如下错误:
[ [email protected] xiangtan_1404]$ chmod 777 file
报错chmod: changing permissions of `file‘: Operation not permitted,权限受限
3)、只有超级用户有权限调用chmod命令或函数设置或修改粘住位
11、粘住位
在早期的unix系统中,如果一个可执行文件的粘住位S_ISVTX做了设置(粘住位st_mode中,前面有提到),那么这个程序第一次执行结束后,这个程序的.text会被保存在交换区,目的是为了下一次执行改程序时,得到快速执行,但是实际上现代unix已经不再需要粘住位这个技术了,我们这里就不再讲解。
12、chown,fchown,lchown函数
12.1函数原型和所需头文件
#include <unistd.h>
int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fd, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
12.2、函数功能:修改文件的所属用户ID或所属组ID。
12.3、函数参数
•const char *path:文件路径
•int fd:文件描述符
•uid_t owner:指定所属用户ID
•gid_t group指定所属组ID
12.4、函数返回值:调用成功返回0,失败返回-1,errno被设置。
12.5、测试用例:略
12.6、注意
1)、我们的chown命令是这两个函数的调用界面。
2)、chown用文件路径操作,fchown功能和chown一样,但是它使用文件描述符操作。lchown也利用文件路径操作,不过它可以操作符号连接文件,chown和fchown却不能。
3)、只有超级用户才能执行chown命令或函数
13、文件长度
13.1、st_size
我们前面学习struct stat结构体时,这个结构体中的st_size被用来描述文件长度,但是这一项只对普通文件、目录和符号连接文件有意义。
13.1.1、普通文件
对于普通文件来说,文件长度可以为0,读到文件末尾时会收到文件结束提示。
13.1.2、目录文件
对于目录文件来说,长度一般是16或512等的整数倍。早期目录文件中的每个目录项是固定大小的长度,但是现在是可变的。
13.1.3、符号链接(又称软连接)文件
ln -s file point_file //建立一个符号连接文件point_file,它指向另file的文件。
ls -al ponit_file 结果如下:
lrwxrwxrwx. 1 linux linux 4 May 3 15:21 point_file -> file
上面符号连接文件point_file文件属性中的4指的是file的字符个数(不包括\0),符号连接文件存放的就是它指向文件的文件名称(相对路径)。
13.2、st_blksize、st_blocks
13.2.1、st_blksize
内核中对于一个文件进行io操作时会选择一个比较好的块长度,早期有些是512,有些是1024,而有些是4096。
通过前面的学习指导,标准io为了提高操作的效率,专门开了一个库缓冲,一般说这个库缓冲的大小就设置为st_blksize,虽然我们在第3章第8小节了解到,最佳的标准io库缓冲大小是8092,但是实际上4096已经和8092效率已经没有太大差别,但是所花费的空间却省了一半。
13.2.1、st_blocks:实际分配的块数。
13.2.3、例子
以普通文件为例。
1)、touch file//创建新文件,但是文件中不写任何数据
2)、编译如下代码
int main(void){ struct stat stat_buf = {0}; lstat("./point_file", &stat_buf); printf("st_size = %d\n", stat_buf.st_size); printf("st_blksize = %d\n", stat_buf.st_blksize); printf("st_blocks = %d\n", stat_buf.st_blocks); return 0; }
3)、运行编译出的可执行文件,结果如下:
st_size = 0 //文件没有数据,所以文件大小为0
st_blksize = 4096 //io操作的块大小4096
st_blocks = 0 //块数是0.因为没有数据
4)、向文件中输入hello,然后编译,再运行可执行文件,结果如下:
st_size = 6 //包含\n在内
st_blksize = 4096 //io操作的块大4096
st_blocks = 8 //给了8块
对于这里8块的说明:
blkcnt_t st_blocks
This is the amount of disk space that the file occupies, measured in units of 512-byte blocks.
The number of disk blocks is not strictly proportional to the size of the file, for two reasons: the file system may use some blocks for internal record keeping; and the file may be sparse—it may have “holes” which contain zeros but do not actually take up space on the disk.
13.3、空洞文件
13.3.1、文件中的空洞
int main(void) { int fd = -1; fd = open("file", O_CREAT|O_RDWR|O_TRUNC, 0664); if(fd < 0) { perror("open file is fail"); exit(-1); } lseek(fd, 8000, SEEK_SET); write(fd, "hello", 5); }
上面的代码,新创建出来的文件file是空的,但是后面的lseek(fd, 8000, SEEK_SET);将文件读写写指针调到了文件的第8000字节处,然后写入字符串“hello”。
磁盘在为普通文件分配空间时是按照4个磁盘块为一个单位进行的,一个磁盘块大小为1024,4个磁盘块实际存储的字节数如果小于4*1024,直接分配4个磁盘块,如果超过了4*1024的字节数,则再分4个磁盘块。
ls -al file
-rw-rw-r--. 1 linux linux 8005 May 3 16:59 file
文件大小为8005字节的文件file至少需要8个磁盘块,但是我们du file 得到的结果却如下:
4 file //磁盘上分了4块
只分了4个磁盘块,因为实际上只有字符串“hello”才存在了磁盘上,前面的都是空洞。空洞在磁盘上实际是不占空间的,所以这个file文件是我们利用lseek做的一个空洞文件,这个在第三章实际上有讲过。
13.3.2、读空洞文件
执行命令cat file > file1,cat命令底层调用的是read函数。
执行du file file1 结果如下:
4 file
8 file1
明显file1大了很多,原因是read函数在读文件中的空洞时,会把它读成实在的0,所以file中那些空洞一旦被read后,空洞就会被读为实实在在的0,然后写在了磁盘上,这些0实际是要占用空间的。
14、文件截断函数
我们学习open时,知道有这么一个标志:O_TRUNC,它的目的是将打开的文件截断(清空)为0,这一小节我们将要学习有关文件截短的函数,它们能够把文件截短为任意长度,O_TRUNC只是文件截断中的一个特例。
14.1函数原型和所需头文件
#include <unistd.h>
#include <sys/types.h>
int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);
14.2、函数功能
将文件截断为所需长度,truncate利用文件路径名操作,ftruncate利用文件描述符操作。
14.3、函数参数
•const char *path:文件路径
•int fd:文件描述符
•off_t length:文件被截断为的长度
14.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
14.5、测试用例
我们用truncate也可以做出空洞文件,就是将制定截断的长度大于文件长度的方式进行截断,那么超过的那部分就形成了空洞。比如前面13.3小节的例子,可以用truncate实现为如下:
int main(void) { int fd = -1; fd = open("file", O_CREAT|O_RDWR|O_TRUNC, 0664); if(fd < 0) { perror("open file is fail"); exit(-1); } ftruncate(fd, 8000);//或则truncate("file", 8000) write(fd, "hello", 5); }
14.6、注意
1)、要求截断的长度length<文件原有长度,截断后的文件中,超过length的内容没有意义,因 为后面的内容就被清掉了。
2)、要求截断的长度length>文件原有长度,超过的部分就会形成空洞,利用这一点可做出空洞
文件。
3)、我们从前面知道,可以利用lseek函数制造空洞文件,但是lseek有个缺点,那就是lseek只 能调动文件的读写指针,但是不能够改写文件内容,所以最后还需要write函数向文件写点数据, 空洞文件最终才能做成,但是truncate函数却不用。
15、文件系统概述
既然我们需要学习文件读写和文件属性等的操作,那么我们就必须要对文件系统有所了解,其实文件系统就是实现对文件进行组织和管理,将所有的文件利用目录这种文件管理容器将所有的文件以树形结构组织起来,方便人们对文件的访问。
文件存放在磁盘上信息包含两部分,一是文件自身的属性信息,二是文件中的数据,对于绝大多数文件来说,只有存有属性信息,而没有具体数据。
15.1、文件系统简单结构图
15.1.1、自举块
自举块也可称为引导块,大小通常为一个磁盘块(1024),起到内核和文件系统启动时的引导作用。
15.1.2、超级块
存放了与整个文件系统管理相关的信息,如文件系统的大小,索引节点表的大小,空闲空间的大小等,我们可以通过ustat系统调用来获取超级块的某些信息。
超级块中包含两个很重要的数据结构,空闲数据块表和空闲inode节点表,专门用于分配和管理数据和inode节点区。
15.1.3、索引节点表(index node list)
索引节点表(inode节点)由若干个磁盘块组成,每个文件在i节点表中都对应一个唯一的索引节点,其中存放了文件的各种属性,这些属性在我们学习stat函数的中的struct stat结构体时都已经做了解到。
我们说7种类型的文件,不管是哪种类型的文件,都对应一个inode节点,但不是所有的文件在磁盘的数据区都存有数据,实际上只有目录和普通文件在数据区才存有真实的数据,其它的文件只在i节点区中存放自己文件属性。
为了访问到数据区存放的数据,inode还存放了很多的指针,这些指针指向了数据区中数据存放的磁盘块,所以我就可以通过inode节点访问到文件在磁盘上具体存放的数了。
但是前提是首先我们要能够找到inode节点,所以为了找到每个inode节点,每个节点实际上都对应了一个inode节点号,目的就是为了索引到inode节点。
一般说每个i 节点的长度是固定的。struct stat结构体中的成员项绝大多数属性都来自i节点,但是文件名字和i节点编号却是来自与目录中的目录项。
15.1.4、数据区
数据区用来存放目录和普通文件对应的数据,文件系统在磁盘上对应的是连续的地址空间,为了效率,数据的存放确是按照块为单位进行操作的,块内数据是连续的,但是块与块之间不见得是一定是连续的,当然读取数据时也是按照块为单位进行的。
数据块的大小一般要适中,因为数据块过大会浪费空间,过小又不能提高效率,一般数据块的大小都定为1024。
目录块中存放目录项列表,这张表说明了在该目下都有哪些文件,每个目录项存放了每个文件的文件名称和该文件唯一对应的i节点编号,文件名用于路径寻访,i节点编号用于索引i节点。数据块中存放的是具体数据,前面也说了是按照块为单位进行存储和访问的。
15.2、文件在文件系统中的具体存放
15.2.1、举例,普通文件在文件系统的存放
1)、存放图示
a)、上图中斜体加下滑线的i节点,代表的是一个普通文件,它的数据存放在了3个 块中, i节点中的数据块指针分别指向了这三个数据块,从图中可以明显的看出这三个数据块不是 连续的,当然也有可能是连续的。
b)、文件真实存放的数据只有一份,并且对应一个唯一的i节点,但是指向这个i节点的却 有两个目录项(file_1和file_2),所以我们就说连接计数等于2。
要删除一个文件,必须等到其连接计数变为0时,各个数据块才被释放,只要还有连接存在,数据块就不会被释放的,那么文件就不会被删除,这就是为什么删除一个目录项的函数名称叫unlink而不是delete的原因了。
我们把上面的这种连接称为硬链接,硬连接的连接计数存放在了stat结构体中st_nlink,另一种连接是符号链接,这两种的区别后面再讲。
c)、目录项中的i节点编号指向的必须是统一文件系统中的i节点,不能够指向跨文件系统
的i节点,因此我们不能创建夸文件系统的硬连接。
d)、我们知道mv命令的作用两个,一个是改文件名字,另一个移动文件,其实这是一回事。
•改名字:比如将file改为file_3,mv隐含的如下步骤:
(1)、建立一个file_3的硬连接,等价于执行命令的ln file file_3
(2)、删除原有名字的连接,等价于执行命令unlink或rm file
一般都是改为当前目录下的什么名字,但是实际上我们完全可以改到其它目录下。
•移动文件:比如mv file到../,mv隐含的步骤如下:
(1)、建立一个../file的硬链接,等价于执行命令的ln file ../file
(2)、删除当前目录下的file硬连接,等价于执行命令unlink或rm ./file
从上面我们可以很清楚看到,修改文件名字和移动文件完全是一回事,都是先建立一个新的硬连接,然后再将原有连接删除,只是改名字的话,新的硬连接往往还是在当前目下,并且名字不同,移动呢,都一般是在不同目录下,但是名字一般不发生变化。
但是不管怎样,实际存放的在数据是不发生更改和移动,所谓的硬连接就是同一个文件可以有很多个不同的名字(多个目录项指向同一文件),但是它们必须属于同一文件系统。
15.2.1、目录文件在文件系统的存放
首先在当前目录kk下创建一个新目录test_dir,mkdir testdir。
目录kk的目录项列表中包含一个test_dir的目录项,该目录项中存入了test_dir目录文件的名字和该文件对应的i节点的节点编号,该节点编号指向了一个i节点,这个i节点指向了数据区的目录块test_dir。
对于任何一个新创建的目录或者说是空目录而言,其连接计数都是2,比如上面新创建test_dir目录,test_dir所在目录项指向了275的i节点,同时test_dir目录中的. 也指向了275的i节点。任何目录都至少包含两个目录项,它们是.. 和. 。
15.3、如何利用文件路径访问文件
比如我想访问/home/linux/kk/file.txt,访问步骤如下:
1)、找到根目录/在磁盘上的位置,/在磁盘上的位置是固定的,这一点很重要。
2)、查找/下的目录项列表,找到文件名为home的目录项,利用该目录项中的i节点号找到 对应的i节点。
3)、利用i节点中的st_mode判断文件的类型,如果发现也是一个目录,进入到这个i节点指 向在数据区的目录(home目录)。
4)、重复2和3的步骤,穿过目录linux和kk。
5)、直到找到非目录文件file.txt为止。
6)、要读取文件数据时,还需要利用file.txt文件对应的inode节点中指向数据块的指针,访问
到数据区中实际存放数据的数据块。写数据时,如果当前数据块写满,则需要重新分配数据 块 继续写数据。
15.4、为什么要用文件描述符
通过前面的学习我们都知道,希望对文件操作时,必须先用open函数打开文件,然后返回一个文件描述符,最后在用这个文件描述符实现对文件的操作,但是为什么不直接利用文件路径,而一定用文件描述符操作呢?原因如下:
1)、对于文件的操作需要很多额外的描述信息,比如我们希望对文件的操作方式是为只读、只
写、可读可写、阻塞或非阻塞等等,以及文件读写指针,文件长度等,这些都需要相应的临时
数据结构来存放,这就要求预先做好相应的准备。
2)、从上面的15.3我们也看出,我们必须通过i节点才能实现对文件的访问,而这个i节点又是 存在了磁盘上的inode节点区,但是访问磁盘的速度是很慢的,再假如路径有很长的话,本身
还要穿过很多目录文件,访问很多目录文件的i节点时,会更加的减慢访问速度,所以我们如果 实现将文件在磁盘的i节点信息区复制到内存中,下一次要使用i节点时,直接使用内存中的副 本即可,这样访问的速度将会大大的提高,但是如果内存中的i节点被更改了的话,磁盘上的对 用的原i节点数据也会被及时更新。
用文件路径先将文件,就是为了后续操做做好从分的准备,从而实现对文件高效的访问。
16、link,unlink,remove,rename函数
从前面的学习中我们知道,可以有很多个目录项指向一个i节点,换句话说一个文件对应了很多名字,每一个指向就是一个硬连接,所以想要删除文件就必须将其连接计数减为0。
我们也学会了ln命令来时创建新的硬连接,比如已有文件ls -al file,结果如下:
-rw-rw-r--. 1 linux root 0 May 4 16:39 file
执行命令ln file new_file,我们就得到了一个新的文件名字new_file,但是它们都指向同一个文件, 执行命令ls -al file new_file,结果如下:
-rw-rw-r--. 2 linux root 0 May 4 16:39 file
-rw-rw-r--. 2 linux root 0 May 4 16:39 new_file
连接计数就从1变为了2,因为file和new_file都指向了同一个文件,当file和new_file在同一个目录下是,名字不允许相同,如果在不同目录下的话,名字可同也可不同。
我们执行rm file或unlink file,file的连接计数被删除。ls -al new_file,结果如下:
-rw-rw-r--. 1 linux root 0 May 4 16:39 new_file
这时连接计数变为了1,我们实现了将file更名为new_file,模拟了mv命令的改名操作。当然执行mv file new_file会来的更为简洁些,但是隐含的操作和上面却是一样,上面ln命令在创建硬连接时,向下的调用是link函数。
16.1、link函数
16.1.1函数原型和所需头文件
#include <unistd.h>
int link(const char *oldpath, const char *newpath);
16.1.2、函数功能:为文件建立一个新的硬连接。
16.1.3、函数参数
•const char *oldpath:原有路径名
•const char *newpath:新的硬链接的路径名
16.1.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
16.1.5、测试用例:略
16.1.6、注意
1)、如果newpath已经存在了,那么就会出错返回。
2)、只有超级用户才可以建立目录的硬连接,因为可以构成循环,但是这可能会导致无限制的死循环,所以普通用户是不能建立目录的硬连接。
3)、硬连接一旦建立,硬连接计数加1。
16.2、unlink函数
16.2.1函数原型和所需头文件
#include <unistd.h>
int unlink(const char *pathname);
16.2.2、函数功能:解除非目录文件的硬连接。
16.2.3、函数参数
•const char *pathname:需删除目录项的路径名
16.2.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
16.2.5、测试用例:略
16.2.6、注意
1)、每删掉一个硬连接,硬连接计数就减1。
2)、为了解除对文件的硬连接,我们对包含该目录项的目录必须有写许可权(修改目录项 列表)和执行许可权(通过该目录项)。
3)、如果文件的连接计数没有被减为0,那么通过其它的硬连接任然可以访问该文件。 当 计数减为0时,但是还有进程打开了文件并且还没有关闭,那么硬连接计数虽然已经减为 0了,但是文件的内容是不会被删除。
4)、所以想要删除文件必须同时满足下两个条件。
a)、连接计数为0
b)、打开文件的进程数也为0
如果以上两个条件都成立的话,文件的实际内容才会被删除。
4)、如果pathname对应的是一个符号链接文件,unlink操作的是符号连接文件本身,而不是符 号连接文件所指向的文件,因为unlink是符号不跟随的函数。
16.2.7、临时文件
我们写程序时,可能经常需要使用到临时文件,但是如果程序中途崩溃的话这个临时文件可能不会被删除,形成临时文件的垃圾,显然是不好的。
通过前面的叙述知道,虽然文件连接计数被减为了0,但是还有进程正打开着文件并且未关闭,那么这个文件实际的内容是不会被删除,也就是说这个文件还是存在的,例如:
int main(void) { int fd = -1; char buf[20] = {0}; fd = open("temp_file", O_RDWR|O_CREAT|O_TRUNC); if(fd < 0) { perror("open temp_file is fail"); exit(-1); } unlink("temp_file"); //立即删除文件 while(1) { write(fd, "hello world", 12); lseek(fd, 0, SEEK_SET); read(fd, buf, sizeof(buf)); printf("buf = %s\n", buf); sleep(1); lseek(fd, 0, SEEK_SET); } return 0; }
在上面的例子中,打开了一个叫temp_file的临时文件后,立即用户unlink函数将其删除,这个临时文件只给当前进程使用,所以它的初始硬连接计数为1,unlink后计数就从1变为了0,但是打开本临时文件的进程还未结束,所以这个文件看起来是被删除了(当前目下是看不到这个文件名字的),但是这个临时文件在磁盘上数据还存在,所以当我们继续利用这个临时文件进行操作,任然是可以的。即便是程序崩溃了,一旦进程结束,那么这个临时文件将不复存在,不会形成垃圾。
16.3、remove函数
16.3.1函数原型和所需头文件
#include <stdio.h>
int remove(const char *pathname);
16.3.2、函数功能:既可以删除目录文件的硬连接,也可以删除非目录文件的硬连接。
16.3.3、函数参数
•const char *pathname:需删除目录项的路径名
16.3.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
16.3.5、测试用例:略
16.3.6、注意
unlink用来删除非目录文件的硬连接(unlink是不能对目录操作),rmdir专门用于删除目录文件的连接,这两个函数都是系统调用,而remove是c提供的库函数,这个函数同时封装了unlink和rmdir这两个系统调用函数。
当检测到是非目录文件时,remove向下调用unlink函数,当检测倒是目录文件时,remove向下调用的是rmdir函数,remove为删除非目录文件和目录文件提供了统一的函数接口。对于目录文件至少都要解除2个硬连接(.和自己的名字),unlink是无法办到的。
Rm命令就是对这个函数的封装。
16.4、rename函数
16.4.1函数原型和所需头文件
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
16.4.2、函数功能:为文件更名。
16.4.3、函数参数
•const char *oldpath:文件旧的路径名
•const char *newpath:文件新的路径名
16.4.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
16.4.5、测试用例:略
16.4.6、注意
更名需要遵守如下规则:
1)、如果oldname指的是一个非目录文件。
a) 、newname已经存在,但是目录文件,rename函数出错返回。
b) 、newname已经存在、但不是目录文件,现将newname对应的目录项删除,然 后再将oldname改为newname。
c) 、对于包含oldname和newname这两个目录项的目录,必须要有写许可权,因为需 要修改这两个目录的目录项列表。
2)、oldname对应的是一个目录文件。
a)、newname已经存在,但是目录文件。
(1)、如果目录为空(只包含. 和.. ),删除该空目录,再将oldname更名为newname。
(2)、如果目录不为空,rename出错返回,出错原因提示目录不为空。
b)、newname已经存在,但是非目录文件,rename出错返回,出错原因提示目录不是目 录文件。
c)、newname的路径前缀中不能包含oldname,如rename(“usr/sys”, “usr/sys/new_name”);
是错误的。
3)、如果oldname = = newname,函数不对文件名和文件内容做任何修改,并且成功返回。
我们知道mv命令有更名功能,但是我们用函数去做和用mv命令去做,在遵守的规则上有些区别,mv也带有很多选项可供选择,这里不在叙述。
如果各位同学觉得上述规则很麻烦,只需记住一点,那就是newname不要指向任何已经存在的文件,可避免上述检查的麻烦。
17、符号连接
17.1、为什么使用符号链接
17.1.1、硬连接的缺点导致引入符号连接
由于硬链接是目录项直接指向i节点,而每个文件系统都有自己独立的i节点区,所以硬连接有如下写限制。
1)、要求连接的目录项和实际的文件(i节点)在同一个文件系统中。
2)、没有办法创建目录的硬连接,因为访问目录时可能会导致死循环。
由于以上硬连接的缺点,所以我么引入符号链接,符号连接实际上是实现对一个文件的间接指向,类似于快捷图标。
17.1.2、硬连接和符号连接的区别图示
1)、硬连接
所有的目录项是并列关系,大家都指向同一个inode节点,每个inode唯一的对应一个文件,一个文件拥有多个路径名(多个目录项指向同一个i节点),所有的目录项和i节点必须同属于一个文件系统。
2)、符号链接
a)、符号连接文件本身也是一种文件,所以我们也可以建立起符号连接文件的硬连接。
b)、也可以建立符号连接的符号连接,多级符号连接。
c)、符号连接实现了对其指向文件的间接访问,并且可以跨文件系统,也就是说符号连 接文件和它所指向文件可以在不同的文件系统中,因为符号连接目录项和符号连接间接
指向的文件的inode是不同inode节点,它们可以分属不同的文件系统。
d)、可以对任何类型的文件建立符号文件,包括目录在内。
17.2、符号跟随与符号不跟随函数
我们前面学习stat、fstat和lstate时,说过前面两个是符号跟随函数,第三个是符号不跟随函数,那么具体解释如下:
1)、符号跟随
函数处理的是符号连接背后所指向的文件,不管这个符号连接有多少级,都是一级一级找下去,直到找到最后最后一个符号连接指向的文件,如果其中任意一个符号连接指向的文件被删除的话,那么函数调用会失败并出错处返回。
2)、符号不跟随
函数直接处理的是符号连接文件本身,而不是它所指向的对象。
17.2.1、常见符号跟随函数
access:按照实际ID进行权限测试的函数,本章函数
chdir:切换目录函数(功能同cd命令),本章后面知识
chown:修改文件所属用户ID和所属组ID,本章
chmod:修改文件权限函数,第三章函数
exec:执行新的可执行文件的函数,进程控制章节
link:建立硬连接函数,本章
mkdir:建立新目录函数,本章
mkfifo:创建有名管道,进程间通信章
open:打开和创建文件,第三章
opendir:打开目录文件,本章
stat:从磁盘读inode取文件属性,本章
fstat:从内存inode结构读取文件属性,本章
truncate、ftruncate:阶段函数,本章
17.2.1、常见符号不跟随函数
lchown:chown的兄弟函数,但是不符号跟随
lstat:stat的兄弟函数,但是不符号跟随
readlink:专门读取符号连接文件中存放的其间接指向的文件路径名,命令readlink就是这个 函数的接口,使用如readlink 符号连接。
remove:可删除所有类型文件硬连接。
unlink:删除非目录文件的硬连接。
rename:文件更名。
17.3、目录的符号链接
请看下列命令:
ln -s . test_dir //给当前目录创建一个符号连接
ls test_dir -al 结果如下:
lrwxrwxrwx. 1 linux root 1 May 5 16:57 test_dir -> .
cd test_dir/;pwd 结果如下:
/home/linux/test_dir
cd test_dir/;pwd 结果如下:
/home/linux/test_dir/test_dir
cd test_dir/;pwd 结果如下:
/home/linux/test_dir/test_dir/test_dir
cd test_dir/;pwd 结果如下:
/home/linux/test_dir/test_dir/test_dir/test_dir
cd test_dir/;pwd 结果如下:
/home/linux/test_dir/test_dir/test_dir/test_dir/test_dir
从上面的黑体字中我们可以看出,对目录已经开始形成了循环,
上图方框代表目录,圆圈代表符号连接,test_dir间接指向了当前目录,所以我们cd test_dir时,就会造成死循环,总是进入到当前目录中。
unlink不能操作目录,但是符号连接并不是目录,如的打叉部分所示,一旦符号连接自己的硬链接被unlink断掉,整个符号连接将不复存在。
对普通用户来说是不能建立目录的硬连接的,因为一旦建立,就无法删除目录项的硬连接,如果造成了目录的死循环就会很麻烦,但是符号连接不会,它很容可以很容易的被unlink删除。
17.4、ls命令显示符号连接文件
1)、-l选项
ls -l显示符号连接文件会有两个提示。
a)、第一个字符l,说明是符号连接。
b)、->,指向某个文件。
2)、-F选项
ls -F,符号连接文件后面会跟一个@符号,说明它是一个符号连接文件。当同时使用-l和-F时,如ls -l -F或ls -lF,F将会被l屏蔽掉,没有用处,因为-l已经能够很清楚的显示出文件的属性,F是多余的。
18、与符号相关的函数,symlink和readlink函数
18.1、symlink函数
18.1.1函数原型和所需头文件
#include <unistd.h>
int symlink(const char *oldpath, const char *newpath);
18.1.2、函数功能:创建符号连接目录项
18.1.3、函数参数
•const char *oldpath:符号连接指向的目录项路径名
•const char *newpath:符号连接目录项的的路径名
18.1.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
18.1.5、测试用例:略
18.1.6、注意
1)、oldpath指向的文件不见得一定要存在,但是最好要存在,否者很多符号跟随的操作会 产生错误。
2)、oldpath对应的文件和newpath对应符号连接文件可以在不同的文件系统中。
18.2、readlink函数
18.2.1函数原型和所需头文件
#include <unistd.h>
ssize_t readlink(const char *path, char *buf, size_t bufsiz);
18.2.2、函数功能:读符号连接的内容,符号连接文件中存放的是指向文件的路径名。
18.2.3、函数参数
•const char *path:符号连接文件的路径名。
•char *buf:存放符号连接指向的文件的路径名字。
•size_t bufsiz:buf的大小
18.2.4、函数返回值:调用成功返回实际读到的buf中的字节数,失败返回-1,errno被设置
18.2.5、测试用例:略
18.2.6、注意
1)、此函数组合了open,read,close三个函数。
2)、返回字节数,但是不包含‘\0’。
19、文件的时间
19.1、与文件相关的时间
在struct stat机构体中与文件时间相关的成员项有如下三个:
1)、对文件内容最后的读取时间:st_atime。
2)、对文件内容的最后的改写时间:st_mtime。
3)、对文件状态的最后改写时间:st_ctime。
19.1、对文件内容最后的读取时间st_atime
1)、含义:文件内容最后一次被访读取的时间。
2)、举例:比如最后一次read文件内容的时间。
3)、ls 如何显示该时间:加-u选项。
4)、作用:便于管理员归档管理文件用,比如删除1月之内没有被读取过的可执行文件,就可 以按照这个时间进行操作。
19.2、对文件内容最后的改写时间st_mtime
1)、含义:文件内容最后一次被改写的时间。
2)、举例:比如最后一次wrie文件内容的时间。
3)、ls 如何显示该时间:缺省情况下(默认情况下)显示的就是这个时间。
4)、作用:便于管理员按照该文件对文件进行归档和管理。
19.3、对文件状态最后的改写时间st_ctime
1)、含义:文件状态(也就是inode节点)最后一次被改写的时间。
2)、举例:比如最后一次chmod、chown或ln的时间,这些函数都会导致i节点被修改。
3)、ls 如何显示该时间:缺省情况下(默认情况下)显示的就是这个时间
4)、作用:便于管理员按照该文件对文件进行归档和管理
5)、注意,对于i节点来说,不存在最后一次被读取的时间,所以access和stat函数并不会修改 st_ctime。
20、mkdir函数和rmdir函数
20.1、mkdir函数
20.1.1函数原型和所需头文件
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
20.1.2、函数功能:创建新目录。
20.1.3、函数参数
•const char *pathname:需创建目录的路径名
•mode_t mode:新目录创建的权限,一般给0775
20.1.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
20.1.5、测试用例:略
20.1.6、注意
1)、新创建的空的目录里面会自动包含. 和.. 两个目录项。
2)、mode会受到进程屏蔽字的修改。
3)、mkdir命令就是对这个函数的封装。
20.2、rmdir函数
20.2.1函数原型和所需头文件
#include <unistd.h>
int rmdir(const char *pathname);
20.2.2、函数功能:删除目录。
20.2.3、函数参数
•const char *pathname:需删除目录的路径名
20.2.4、函数返回值:调用成功返回0,失败返回-1,errno被设置
20.2.5、测试用例:略
20.2.6、注意
1)、此函数想要执行成功,被删除的目录必须为空(只包含. 和.. 两个目录项)。
2)、与目录有关的所有的用连接一次性被减为0。
3)、如果检测到连接数为0了,但是还有进程正打开了这个目录并且进程没有结束,
那么这个目录在数据区对应的目录块是不会被删除的,这一点类似于unlink函数操作非
目录文件。
21、打开目录,读目录等函数
21.1、opendir函数
21.1.1函数原型和所需头文件
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name);
21.1.2、函数功能:打开目录文件。
21.1.3、函数参数
•const char *name:需打开目录的路径名
21.1.4、函数返回值:成功返回指向目录文件流的目录文件指针,失败返回NULL,errno被设置。
21.1.5、测试用例:略
21.1.6、注意:openddir是一个库函数
21.2、readdir函数
21.2.1函数原型和所需头文件
#include <dirent.h>
struct dirent *readdir(DIR *dirp);
21.2.2、函数功能:读目录文件里的目录项。
21.2.3、函数参数
•DIR *dirp:opendir打开目录文件时返回的目录流指针。
21.2.4、函数返回值:成功则返回一个指针指向dirent结构体,返回NULL的话有如下两种情况:
1)、读到目录文件的末尾则返回NULL,errno保持原有设置,不发生变化。
2)、函数调用失败也返回NULL,但是errno被设置。
21.2.5、测试用例:略
21.2.6、注意
dirent结构定义在了<dirent.h>头文件中,它的具体成员实现如下:
struct dirent
{
ino_t d_ino; /* i节点编号 */
off_t d_off; /* 到下一个目录项的地址偏移 */
unsigned short d_reclen; /* 目录项里面存放的信息的长度 */
unsigned char d_type; /*文件类型,不是所有系统都支持,
并且在i节点中有文件类型的说明 */
char d_name[256]; /* 文件名字 */
};
1)、上面这个结构是用来存放目录项的相关信息的,其中最重要的就是i节点编号和文件 名字。
2)、我们前面说过,早期目录项的大小是固定的,比如给目录项16个字节,14个字节用 来表示文件名,2个字节来存放i节点编号,但是对于现在的系统来说就不再是了,为了存 放各种长度不一的文件名,现在的目录项大小会随着文件名的长度的大小发生变化,目录项 的存放形式类似于我们前面学过的数组。
数组的每一项的大小是相同的,但是这里的每一项的空间大小是可变的(随名字的长度变化),所以访问下一个目录项,需要用到一个偏移,这个偏移说明了从当前目录项的开始处移动多少字节可以访问到下一个目录项,这个偏移就是d_off,d_type代表的是目录项中实际存放的记录的长度,d_type <= d_off。
3)、readdir是一个库函数。
21.3、rewinddir函数
21.3.1函数原型和所需头文件
#include <sys/types.h>
#include <dirent.h>
void rewinddir(DIR *dirp);
21.3.2、函数功能:将对目录的读写指针调到目录项列表的第一个目录项最开始处。
21.3.3、函数参数:DIR *dirp:opendir打开目录文件时返回的目录文件流指针。
21.3.4、函数返回值:无返回值
21.3.5、测试用例:略
21.3.6、注意:它是一个库函数
21.4、closedir函数
21.4.1函数原型和所需头文件
#include <sys/types.h>
#include <dirent.h>
void closedir(DIR *dirp);
21.4.2、函数功能:关闭目录文件。
21.4.3、函数参数:DIR *dirp:opendir打开目录文件时返回的目录文件流指针。
21.4.4、函数返回值:无成功返回0,失败返回-1,errno被设置。
21.4.5、测试用例:略
21.4.6、注意:它是一个库函数
22、chdir、和getcwd函数
22.1、chdir函数
22.1.1函数原型和所需头文件
#include <unistd.h>
int chdir(const char *path);
22.1.2、函数功能:切换目录。
22.1.3、函数参数:const char *path:路径。
22.1.4、函数返回值:无成功返回0,失败返回-1,errno被设置。
22.1.5、测试用例:略
22.1.6、注意:
当前工作目录与我们的进程息息相关,有的时候我们进程需要从当前工作目录切换到其它的目录下,这个时候就需要调用到这个函数。
22.2、chdir函数
22.2.1函数原型和所需头文件
#include <unistd.h>
char *getcwd(char *buf, size_t size);
22.2.2、函数功能:获取进程的当前工作目录。
22.2.3、函数参数:
const char *path:存放当前路径(一个字符串)。
size_t size:buf允许的大小
22.2.4、函数返回值:无成功返回0,失败返回-1,errno被设置。
22.2.5、测试用例:略
22.2.6、注意:它是一个库函数。
23、自己简单实现ls命令
23.1、功能要求:
1)、显示指定文件的属性
2)、如果是目录,需要显示目录里面的文件的属性,如果目录里面还有目录,那么也要显示那
些目录下的文件的属性,递归实现,知道所有的目录下的文件属性都被现实位置。
23.2、例子代码
/* 所需头文件如下 */ #include <stdio.h> #include <unistd.h> #include <string.h> #include <errno.h> #include <sys/types.h> #include <sys/stat.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <dirent.h> #include <errno.h> #include <pwd.h> #include <grp.h> #include <time.h> /* 定义一个存放文件类型的数组 */ int file_type[] = { S_IFSOCK, S_IFLNK, S_IFREG, S_IFBLK, S_IFDIR, S_IFCHR, S_IFIFO, }; /* ls :显示文件属性*/ void display_file_distribute_fun(char *file_path) { int i = 0, ret = -1; struct stat stat_buf = {0}; char buf[100] = {0}; //存放文件属性,这些属性都已字符串形式存放 char *pos = NULL; // struct passwd *pass_p = NULL; struct group *gir_p = NULL; char tim_buf[40] = {0}; char file_path_name[50] = {0}; struct tm *tm_p = NULL; /* fstat: 利用文件描述副操作,从v节点中的i节点信息中 * 读取文件属性,因为之前的open函数已经将文件的属性 * 从磁盘中读到了v节点中的i节点信息中 * state: 这个函数和fstat功能是一样的,但是它直接的从 * 磁盘中读取文件属性 * lstat: 前面的fstat和state都是符号链接跟随函数,但是 * lstat不是的,专门用于显示符号链接文件的属性 */ ret = lstat(file_path, &stat_buf); if(-1 == ret) { fprintf(stderr, "stat \"%s\" is fail: %s\n", file_path, strerror(errno)); exit(-1); } /* 从st_mode中获取文件的类型,存入buf中 */ for(i=0; i<sizeof(file_type)/4 ; i++) { if((stat_buf.st_mode & S_IFMT) == file_type[i]) { buf[0] = "sl-bdcf"[i]; //将文件类型存到buf中 } } /* 判断是不是目录文件,如果是目录文件的话,需要递归显示,这里除去. 和.. ,否者 * 会造成递归死循环下去 */ if(‘d‘==buf[0] && strcmp(file_path, ".")!=0 && strcmp(file_path, "..")!=0) { DIR *dir_p = NULL; char pwd[300] = {0}; //保存当前目录用 struct dirent *dirent_p = NULL; //存放读到的目录项信息 /* 切换到这个目录下面 */ getcwd(pwd, sizeof(pwd));//保存当前目录 chdir(file_path);//切入该目录中 /* 打开目录 */ dir_p = opendir(".");//打开目录文件 if(NULL == dir_p) { fprintf(stderr, "opendir \"%s\" is fail: %s\n", ".", strerror(errno)); exit(-1); } while(1) { /* 如果返回的时NULL,目录项被读完了 */ dirent_p = readdir(dir_p); if(NULL == dirent_p) break; //递归调用,显示目录项下文件的属性 display_file_distribute_fun(dirent_p->d_name); } printf("======= dir %s over =======\n\n", file_path); chdir(pwd);//切换回上一集目录 } /* 对符号链接文件的名字做处理,将名字修改为 XXX -> YYY形式 */ if(buf[0] == ‘l‘) { char t_buf[30] = {0}; readlink(file_path, t_buf, sizeof(t_buf)); sprintf(file_path_name, "%s -> %s", file_path, t_buf); } else strcpy(file_path_name, file_path); //否者不用修改 /* 从st_mode中读取出文件的权限,存入buf中 */ for(i=0; i<9; i++) { if(stat_buf.st_mode & (1<<(8-i))) { buf[1+i] = "rwxrwxrwx"[i]; } else buf[1+i] = ‘-‘; if(stat_buf.st_mode & (1<<10)) buf[3] = ‘s‘; if(stat_buf.st_mode & (1<<11)) buf[6] = ‘s‘; } /* 将数值的uid和gid转换为我们能识别的字符串,如将500转为linux,存入buf中 * 如下函数在后续章节中陆续会学到,大家这里先了解 */ pos = &buf[strlen(buf)]; //找到在buf中续接存放后续属性信息的位置 tm_p = localtime(&(stat_buf.st_mtime)); //将数值秒数转为独立的年、月、日、时、分、秒 pass_p = getpwuid(stat_buf.st_uid); //将数值的uid转换为字符串 gir_p = getgrgid(stat_buf.st_gid); //将字符的gid转换为字符串 //将年月日时分秒组织为我们需要的格式 strftime(tim_buf, sizeof(tim_buf), "%b %d %H:%M", tm_p); /* 将连接计数,所属用户uid,所属组gid,文件大小,文件内容最户一次修改的时间, * 文件名字等文件属性的信息也续接存放到buf中pos所指向的位置*/ sprintf(pos, ". %d %s %s %ld %s %s", stat_buf.st_nlink, pass_p->pw_name, gir_p->gr_name, stat_buf.st_size, tim_buf, file_path_name); printf("%s\n", buf); //从buf中打印出文件属性 return; } /* 主函数 */ int main(int argc, char *argv[]) { int i = 0; /* 如果忘记输入需被显示属性的文件的名字的话,报错 */ if(1 == argc) { printf("./a.out file_name ...\n"); exit(-1); } /* 显示,命令行输入文件的属性 */ for(i=1; i<argc; i++) display_file_distribute_fun(argv[i]); return 0; }