UNIX中有句话“一切皆为文件”,因此全方位了解文件属性是非常必要的。
stat函数如下,使用stat函数最多的可能是ls -l命令了
int stat(const char *path, struct stat *buf);
int fstat(int fd, struct stat *buf);
int lstat(const char *path, struct stat *buf);
struct stat {
dev_t st_dev; /* ID of device containing file */
ino_t st_ino; /* inode number */
mode_t st_mode; /* protection */
nlink_t st_nlink; /* number of hard links */
uid_t st_uid; /* user ID of owner */
gid_t st_gid; /* group ID of owner */
dev_t st_rdev; /* device ID (if special file) */
off_t st_size; /* total size, in bytes */
blksize_t st_blksize; /* blocksize for file system I/O */
blkcnt_t st_blocks; /* number of 512B blocks allocated */
time_t st_atime; /* time of last access */
time_t st_mtime; /* time of last modification */
time_t st_ctime; /* time of last status change */
};
本章围绕stat的属性进行介绍UNIX文件相关的属性。
1. 检测文件类型
我们知道有很多文件类型,如普通文件、目录文件、字符特殊文件、块文件、FIFO、套接口、符号链接文件(stat函数检测不出来,得是lstat函数才行)文件类型的信息包含在st_mode,可以用下表的宏确定文件类型。
S_ISREG(m) //is it a regular file
S_ISDIR(m) //directory
S_ISCHR(m) // character device
S_ISBLK(m) // block device
S_ISFIFO(m) //FIFO (named pipe)
S_ISLNK(m) //symbolic link
S_ISSOCK(m) //socket
下面看一个程序
#include <unistd.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <stdlib.h>
#include <errno.h>
int
main(int argc, char *argv[])
{
int i;
struct stat buf;
char *ptr;
for (i = 1; i < argc; i++) {
printf("%s: ", argv[i]);
if (lstat(argv[i], &buf) < 0) {
perror("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_ISLNK(buf.st_mode))
ptr = "symbolic link";
else if (S_ISSOCK(buf.st_mode))
ptr = "socket";
else
ptr = "** unknown mode **";
printf("%s\n", ptr);
}
exit(0);
}
2. 设置用户ID(SUID)和设置用户组ID(SGID)
详情见uid,suid
3. 文件访问权限
st_mode也包含了对文件的访问权限,下面看下文件权限。
S_IRUSR //owner has read permission
S_IWUSR //owner has write permission
S_IXUSR //owner has execute permission
S_IRWXG //mask for group permissions
S_IRGRP //group has read permission
S_IWGRP //group has write permission
S_IXGRP // group has execute permission
S_IRWXO //mask for permissions for others (not in group)
S_IROTH //others have read permission
S_IWOTH //others have write permission
S_IXOTH //others have execute permission
目录的读权限允许获取该目录中所有文件名的列表,执行权限使可以通过该目录(搜索目录,寻找一个特定文件名)
1. 一个文件的读权限,这与open函数的O_RDONLY和O_RDWR有关
2. 一个文件的写权限,这与open函数的O_WRONLY和O_RDWR有关
3. 为在open函数中创建一个新文件,必须对该目录具有写权限和执行权限
4. 为删除一个文件,必须对包含该文件的目录具有,但对该文件则不需要读,写权限
- 文件测试
进程每次打开、创建或删除文件时,内核就进行访问权限测试,而这种测试可能涉及文件的所有者(st_uid,st_gid)、进程的有效ID(euid,egid)以及进程的附加组ID。其中两个所有者ID是文件的性质,两个有效ID和附加组ID是进程的性质。内核进行的测试如下
(1)若进程euid是root,则干什么都行;
(2)若进程euid是等于uid,按照相应用户的权限读写;
(3)若进程的egid或进程的附加组id等于文件gid,按照组相应的权限读写;
(4)若其他用户适当的访问权限被设置,则允许访问,否则拒接访问;
4. 新文件和目录的所有权
当用open或creat创建文件时
1. 新文件的UID设置为进程的有效用户ID(euid)
2. 组ID可以是进程的有效组ID或它所在目录的组ID
(/var/spool/mail)
如下测试例子
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
int main(void)
{
if (creat("test.txt",0666) < 0)
{
perror("create file error");
exit(1);
}
printf("create file success\n");
exit(0);
}
测试过程
#测试1
$ ./a.out //以david用户执行,euid=uid=david
create file success
$ ll test.txt
-rw-rw-r-- 1 david david 0 Oct 16 09:26 test.txt
$ rm test.txt //删除test.txt
#测试2
$ ./a.out //以root用户执行,euid=uid=root
create file success
$ ll test.txt
-rw-r--r-- 1 root root 0 Oct 16 09:29 test.txt
#测试3
$ chmod u+s a.out //设置SUID
$ rm test.txt //删除test.txt
$ ll a.out
-rwsrwxr-x 1 david david 8666 Oct 16 09:26 a.out
$ ./a.out //以root用户执行,uid=root,euid=david
create file success
$ ll test.txt
-rw-r--r-- 1 david root 0 Oct 16 09:35 test.txt
从上测试可看出,新创建的文件的uid始终和euid一样。
5. access函数
当用open函数打开一个文件时,内核以进程的有效用户ID和有效组ID为基础执行其访问权限测试,但当一个进程使用SUID或SGID作为另一个用户运行时,可能会需要测试其实际用户(uid)和gid能否访问一个给定的文件,因此需要access函数,access函数是按实际用户ID和实际组ID进行访问测试的。
#include <unistd.h>
int access(const char *pathname, int mode);
/*
成功返回0,失败返回-1
mode常量:
R_OK:测试读权限
W_OK:测试写权限
X_OK:测试执行权限
F_OK:测试文件是否存在
*/
测试例子
#include <errno.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
int main(int argc, char *argv[])
{
if (argc != 2)
{
printf("uasge: ./aout");
exit(1);
}
if (access(argv[1],R_OK) < 0) //测试实际uid能否访问shadow文件
{
perror("access error");
}
else
printf("read access OK\n");
if (open(argv[1],O_RDONLY) < 0) //测试进程的有效id(euid)能否访问shadow文件
{
perror("open error");
exit(1);
}
else
printf("open for reading OK\n");
exit(0);
}
测试1,简单测试功能
# ll a.out
-rwxr-xr-x. 1 david david 5358 Apr 14 06:53 a.out
# ./a.out a.out //一般用户,此时a.out的euid和uid都是david
read access OK
open for reading OK
测试2,更改a.out为root用户
# ll /etc/shadow
-r--------. 1 root root 1047 Mar 13 08:00 /etc/shadow
# ./a.out /etc/shadow // 以david用户执行,uid是david,euid=uid
access error: Permission denied
open error: Permission denied
$ chown root a.out //更改a.out用户是root
# ll a.out
-rwxr-xr-x. 1 root david 5338 Apr 14 07:01 a.out
# ./a.out /etc/shadow //以david用户执行,此时进程的uid是david,euid=uid,也是david
access error: Permission denied
open error: Permission denied
$ ./a.out /etc/shadow //以root用户执行,此时进程的uid是root,euid=uid,也是root
read access OK
open for reading OK
测试3,设置EUID位
$ chmod u+s a.out //并打开euid,将用户的进程的有效用户ID(euid)设置为a.out所有者的id,为root
# ll a.out
-rwsr-xr-x. 1 root david 5338 Apr 14 07:01 a.out
# ./a.out /etc/shadow //以david用户执行,uid是david,euid是root
access error: Permission denied
open for reading OK
$ ./a.out /etc/shadow //以root用户执行,此时uid是root,euid是root
read access OK
open for reading OK
注意进程的uid就是执行当前文件的用户(并不是当前用户所有者的ID),而euid一般是等于uid的,但如果设置了euid,则euid等于可执行文件所有者的ID,所以判读一个进程的uid,euid,先看谁执行的进程,则uid就是谁;若没有设置euid,则euid=uid,若设置了euid,euid=可执行文件的所有者ID
6. umask函数
umask函数为进程设置文件创建屏蔽字,并返回以前的值。(少数几个没有出错返回函数的一个),umask设置创建文件时要屏蔽的位。
#include <sys/stat.h>
mode_t umask(mode_t cmask);
/*返回值:以前的文件创建屏蔽字*/
当进程创建文件或目录时,一定会使用umask创建屏蔽字,如下栗子
#include <unistd.h>
#include <sys/stat.h>
#include <errno.h>
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <sys/types.h>
int main(void)
{
umask(0);
if (creat("foo",0666) < 0)
perror("create error for foo");
umask(0066);
if (creat("bar",0666) < 0)
perror("create error for foo");
exit(0);
}
测试过程
$ umask //打印当前mask值
0002
$ ll foo bar
-rw------- 1 zty zty 0 Oct 16 09:42 bar
-rw-rw-rw- 1 zty zty 0 Oct 16 09:42 foo
通常umask命令并不影响父进程(常常是shell)的屏蔽字,所有shell都内置umask命令,用户可以设置umask值控制他们创建文件的默认权限。在写创建新文件程序时,若需确保指定的访问权限位已激活,需在进程运行时修改umask值,否则会继承父进程(通常是shell)的mask值
$ umask -S //打印符号形式
u=rwx,g=rwx,o=rx
$ umask 027 //更改文件模式创建屏蔽字
$ umask -S
u=rwx,g=rx,o=
7. chmod、fchmod
#include <sys/stat.h>
int chmod(const char *path, mode_t mode); //在指定的文件上进行操作
int fchmod(int fildes, mode_t mode); //已打开的文件进行操作
/*成功返回0,失败返回-1*/
为改变一个文件的权限位,进程的euid必须等于文件的所有者id,或进程必须具有root权限
8. chown、fchown、lchown函数
#include <unistd.h>
int chown(const char *path, uid_t owner, gid_t group);
int fchown(int fildes, uid_t owner, gid_t group);
int lchown(const char *path, uid_t owner, gid_t group);
/*成功返回0,失败返回-1*/
- 除了引用文件是符号链接外,这三个函数操作相似,在符号链接的情况下,lchown更改符号链接本身的所有者,而不是该符号链接所指向的文件
- owner、group中任意一个是-1,则对应ID不变
- 谁能更改UID,GID(root?进程拥有者?)
9. 文件长度、文件截短
stat结构成员st_size表示以字节为单位的文件长度;
1. 对目录:文件长度通常是一个数的倍数(16或512);
2. 对符号链接,文件长度是文件名中的实际字节数;
如今UNIX系统大都提供st_blksize(best I/O block size)和st_blocks(number of disk blocks allocated),其中,第一个是对文件I/O较适合的块长度,第二个是所分配的实际512字节块的数量(依赖于版本实现),当我们将st_blksize用于读操作时,读一个文件所需要的时间量最少,为了效率的缘故,标准I/O库也试图以此读、写st_blksize个字节。
- 文件的空洞
空洞是由设置的偏移量超过文件尾端,并写了某些数据后造成的。
截图103页。
这两个函数将现有的文件长度截短为length字节,如将一个文件清空为0
#include <unistd.h>
int truncate(const char *path, off_t length);
int ftruncate(int fildes, off_t length);
/*成功返回0,出错返回-1*/
10. 文件系统
主要讲i节点和指向i节点的目录项,说不清。。。
11. link、unlink、remove和rename函数
一个文件可以有多个指向i节点,创建一个指向现有文件链接的函数
int link(const char *oldpath, const char *newpath);
/*成功返回0,失败返回-1*/
不能有指向一个目录的硬链接,因为这样可能在文件系统中存在循环,但是符号链接却可以,见为什么硬链接不能指向目录, 因此很多文件系统不允许对目录实现硬链接
删除一个现有的目录项函数
#include <unistd.h>
int unlink(const char *pathname);
/*成功返回0,失败返回-1*/
该函数删除目录项,并将由pathname所引用的文件链接计数减1;如果pathname是符号链接,那么unlink删除该符号链接,而不会删除由该链接所引用的文件。关闭一个文件时:内核先检查打开该文件的进程数,如果为0,接着检查其链接数,如果也为0,则删除该文件的内容。unlink这种性质经常被程序用来确保即使在该程序崩溃时,它所创建的临时文件也不会遗留下来,用open或create创建一个文件,然后立即调用unlink。因为该文件仍旧是打开的,所以不会对其内容删除,只有当进程关闭或终止时,该文件内容才会被删除。
例子,打开一个文件然后unlink它
同样可以用remove函数解除对一个文件或目录的链接,对于文件,remove功能和unlink相同,对于目录,remove功能和rmdir相同。
#include <stdio.h>
int remove(const char *pathname);
文件或目录用rename更名
#include <stdio.h>
int rename(const char *oldpath, const char *newpath);
/*成功返回0,失败返回-1*/
- 如果oldname指的是一个文件而不是目录,那么为该文件或符号链接更名;如果newpath存在,则不能是一个目录;若newpath存在且不是目录,则会删除原newpath文件,并将oldpath的名更改为newpath
- 如果oldname指的是一个目录,那么为该目录更名;原理同2
- 如果oldname指的是一个符号链接,则为符号链接更名,而不是它所引用的文件;
- 如果oldname和newname相同,则不做任何处理
12. 符号链接
符号链接是指向一个文件的间接指针,与硬链接不同
1. 硬链接通常要求链接和文件位于同一文件系统汇总
2. 只有具有root权限的进程才能创建指向目录的硬链接
对于符号链接以及它所指向的对象无文件系统的限制,符号链接一般用于将一个文件或整个目录结构移到系统中的另一个位置。
注意
当使用以名字引用文件的函数时,应当了解该函数是否处理符号链接,即是对符号链接处理还是对符号链接指向的文件处理
符号链接引可能在文件系统中引入循环,如下操作
$ mkdir foo
$ touch foo/a
$ ln -s foo foo/foo.soft
这样打不开该符号链接,即foo.soft文件
#file并不存在,但可以创建成功
$ ln -s file file.soft
13. symlink和readlink函数
创建符号链接
#include <unistd.h>
int symlink(const char *oldpath, const char *newpath);
/*成功返回0,失败返回-1*/
由于open函数是跟随符号链接(即指向符号链接的文件,而不是针对符号链接本身操作),因此需要读取链接文件中的名字(并不是读符号链接指向的文件)
#include <unistd.h>
ssize_t readlink(const char *path, char *buf, size_t bufsiz);
/*成功返回到读到的字节数,出错返回-1
buf返回的符号链接内容不以null字符终止
*/
14. 文件的时间,utime函数
每个文件保持有三个时间字段,如图
注意修改时间(st_mtime)和更改时间(st_ctime)之间的区别,修改时间是文件内容最后一次被修改的时间,更新状态时间是该文件的i节点最后一次被修改的时间,由上可知,很多影响到i节点的操作,如更改文件的访问权限,uid,连接数,但并没有更改文件的实际内容。i节点中的信息和文件中的实际内容是分开存放的,注意系统并不保存对一个i节点的最后一次访问时间,所以access和stat函数并不更改这三个时间中任何一个
一个文件的访问和修改时间可以用utime函数更改。
15. mkdir和rmdir函数
#include <sys/stat.h>
#include <sys/types.h>
int mkdir(const char *pathname, mode_t mode);
/*成功返回0,失败返回-1*/
该函数创建一个新的空目录,所指定的文件权限mode由进程的文件模式创建屏蔽字修改
#include <unistd.h>
int rmdir(const char *pathname);
/*成功返回0,失败返回-1*/
该函数可以删除一个空目录(空目录是只包含.和..这两项的目录)
16. 读目录
#include <sys/types.h>
#include <dirent.h>
DIR *opendir(const char *name); //成功返回指针,失败返回NULL
struct dirent *readdir(DIR *dirp); //成功返回指针,失败返回NULL
void rewinddir(DIR *dirp);
int closedir(DIR *dirp); //成功返回0,失败返回-1
long telldir(DIR *dirp); //返回值:与dp相关联的目录中的当前位置
void seekdir(DIR *dirp, long offset);
其中头文件
struct dirent
{
ino_t d_ino; File serial number.
char d_name[]; Name of entry.
}
例子,递归遍历目录层次结构
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdlib.h>
#include <stdio.h>
#include <errno.h>
#include <string.h>
#include <dirent.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
int main(int argc, char *argv[])
{
DIR *dir;
struct dirent *de;
if (argc != 2)
{
fprintf(stderr,"a single argument is required\n");
exit(EXIT_FAILURE);
}
if ( (dir=opendir(argv[1])) == NULL)
ERR_EXIT("cant open dir");
while ( (de=readdir(dir)) != NULL)
{
fprintf(stdout,"%s\n",de->d_name);
}
closedir(dir);
exit(EXIT_SUCCESS);
}
17. chdir、fchdir和getcwd函数
以下函数可以更换当前目录
#include <unistd.h>
int chdir(const char *path);
int fchdir(int fd);
/*成功返回0,,失败返回-1*/
由于当前目录是进程的一个属性,所以它只影响调用chdir的进程本身,而不影响其他进程,如下例子
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
int main(void)
{
if (chdir("/tmp") < 0)
{
perror("chdir");
exit(1);
}
printf("chdir to /tmp succeeded\n");
exit(0);
}
执行此程序,并没有切换到/tmp目录,但程序却显示“chdir to /tmp succeeded”;因为shell创建了一个子目录,由该子进程具体执行了次程序;可见若要改变shell进程的工作目录,需shell自己调用chdir函数。
如果内核保持当前目录的信息,直接取出即可;可是内核为每个进程只保存指向该目录v节点的指针等目录本身信息,并不保存目录的完整路径名,因此需要getcwd函数
#include <unistd.h>
char *getcwd(char *buf, size_t size);
/*成功返回buf,失败返回NULL*/
getcwd可以保存当前目录,然后当应用程序进入其他目录时,可返回到原目录;而fchdir则更容易,用open打开当前工作目录,然后保存文件描述符号,当希望返回到原工作目录时,只要将文件描述符传递给fchdir即可。