上一篇总结了文件系统的I/O操作,本篇继续总结文件系统的其他特性,包括文件和目录,同时还总结Linux系统的用户管理。本篇也是对日常使用和管理一篇的扩充,从编程角度重新总结Linux系统的这些特性。
Contents
- 文件
- 文件类型
- 文件所有者
- 文件访问权限
- 文件长度
- 文件时间
- 文件系统
- 符号链接
- 目录
- 设备特殊文件
- 用户
- 口令文件
- 组文件
- 附加组
- 用户登录名
- 系统标识
- 日期和时间
文件
可以使用 stat 函数族得到和文件有关的信息结构。
#include <sys/types.h> #include <sys/stat.h> #include <unistd.h> /* 获取和文件相关的信息结构,当文件是符号链接时,lstat返回该符号链接的信息 * @return 成功返回0,出错返回-1 */ int stat(const char *path, struct stat *buf); int fstat(int fd, struct stat *buf); int lstat(const char *path, struct stat *buf);
函数会将文件的信息填入 buf 指针指向的预分配的结构。结构 stat 的定义为:
struct stat { dev_t st_dev; /* 设备ID(文件系统) */ ino_t st_ino; /* i节点号 */ 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; /* 文件系统I/O的块大小 */ blkcnt_t st_blocks; /* 分配的512B的块数 */ time_t st_atime; /* 最近访问时间 */ time_t st_mtime; /* 最近修改时间 */ time_t st_ctime; /* 最近状态改变时间 */ };
文件类型
Linux把文件系统中的项统称为文件,文件有七种类型:
- 普通文件。即通常的文件,Linux下内核不区分数据为文本还是二进制。一个例外是二进制的可执行目标文件,它遵循前面链接一篇中介绍的格式。
- 目录文件。它包含其他文件的名字和指向这些文件有关信息的指针。
- 符号链接。它指向另一个文件。
- 块特殊文件。它提供对设备带缓冲的访问,每次按固定长度进行。
- 字符特殊文件。它提供对设备不带缓冲的访问,每次长度可变。
- FIFO。即命名管道,用于进程间通信。
- 套接字。用于进程间通信。
系统中的设备必是块特殊文件或字符特殊文件。
文件类型包含在 stat 结构的 st_mode 中,可以将它传递给下面的宏来确定文件类型:
S_ISREG() /* 普通文件 */ S_ISDIR() /* 目录文件 */ S_ISLNK() /* 符号链接 */ S_ISBLK() /* 块特殊文件 */ S_ISCHR() /* 字符特殊文件 */ S_ISFIFO() /* FIFO */ S_ISSOCK() /* 套接字 */
还可以将进程间通信对象表示为文件,将指向 stat 结构的指针传递给下面的宏可以确定它们的类型:
S_TYPEISMQ() /* 消息队列 */ S_TYPEISSEM() /* 信号量 */ S_TYPEISSHM() /* 共享存储 */
文件所有者
每个文件都有所有者和组所有者,保存为用户ID和组ID,对应 stat 结构中的 st_uid 和 st_gid 。
和进程相关联的ID有:
- 实际用户ID、实际组ID。它们在登录时取自口令文件中的登录项,表明我们是谁。
- 有效用户ID、有效组ID、附加组ID。决定了我们的文件访问权限。
- 保存的设置用户ID、保存的设置组ID。在程序执行时包含有效用户ID和有效组ID的副本。
一般程序执行时,进程的有效用户ID等于实际用户ID,有效组ID等于实际组ID。但 st_mode 中有设置用户ID位和设置组ID位,设置它们,在该文件执行时,会分别将进程的有效用户ID和有效组ID设置为文件的用户ID和组ID。因此可执行文件可以通过这种办法提升自己的运行时权限,如 passwd 命令。
文件的设置用户ID位和设置组ID位通过将 st_mode 传给 S_ISUID 和 S_ISGID 宏来测试。
进程在打开、创建或删除文件时,内核会测试文件访问权限,按如下步骤进行:
- 若进程的有效用户ID为0,即超级用户,则允许访问。
- 若进程的有效用户ID等于文件的用户ID,根据文件的用户访问权限决定是否允许访问。
- 若进程的有效组ID或附加组ID之一等于文件的组ID,根据文件的组访问权限决定是否允许访问。
- 以上都不满足,根据文件的其他访问权限决定是否允许访问。
创建新文件时,新文件的用户ID设为进程的有效用户ID,新文件的组ID设为进程的有效组ID或目录的组ID。
也可以使内核基于进程的实际用户ID和实际组ID测试文件访问权限,使用 access 函数实现。
#include <unistd.h> /* 以进程的实际用户ID和实际组ID测试文件访问权限 * @return 成功返回0,出错返回-1 */ int access(const char *pathname, int mode);
参数说明:
- mode
-
要测试的权限,可取值为:
- R_OK ,测试读权限。
- W_OK ,测试写权限。
- X_OK ,测试执行权限。
- F_OK ,测试文件是否存在。
例:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <fcntl.h> #include "error.h" int main(int argc, char *argv[]) { if (argc != 2) err_quit("usage: a.out <pathname>"); if (access(argv[1], R_OK) < 0) err_ret("access error for %s", argv[1]); else printf("read access OK\n"); if (open(argv[1], O_RDONLY) < 0) err_ret("open error for %s", argv[1]); else printf("open for reading OK\n"); exit(0); }
编译之后设置可执行文件的设置用户ID位,可以看到实际用户虽然不能读 /etc/shadow ,但 open 函数可以打开它。
# chown root a.out # chmod u+s a.out # exit $ ls -l a.out -rwsr-xr-x 1 root yeolar 6775 May 8 13:11 a.out $ ls -l /etc/shadow -rw-r----- 1 root shadow 1015 Mar 12 14:42 /etc/shadow $ ./a.out /etc/shadow access error for /etc/shadow: Permission denied open for reading OK
可以用 chown 函数族改变文件的用户ID和组ID。
#include <unistd.h> /* 改变文件的用户ID和组ID * @return 成功返回0,出错返回-1 */ 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);
owner 或 group 为-1时,对应的ID不变。
对于符号链接, lchown 改变符号链接的用户ID和组ID,而不是符号链接指向的文件。
文件访问权限
文件有9个访问权限位,分为三类,在 st_mode 中的屏蔽位如下:
有关文件访问权限,有一些使用规则:
- 通过文件名打开任一文件,要求对文件名包含的每个目录都有执行权限。
- 文件的读权限决定了是否能打开该文件进行读操作。
- 文件的写权限决定了是否能打开该文件进行写操作。
- open 函数对文件指定 O_TRUNC 标志要求对该文件具有写权限。
- 在目录中创建新文件或删除文件,要求对该目录具有写和执行权限。
- 用 exec 函数族执行文件,要求对该文件具有执行权限,且文件须为普通文件。
umask 函数可以为进程设置文件模式的屏蔽字。进程在创建文件时,使用该屏蔽字关闭 open 和 creat 函数的 mode 参数的相应位。
#include <sys/types.h> #include <sys/stat.h> /* 为进程设置文件模式创建屏蔽字 * @return 返回之前的文件模式创建屏蔽字 */ mode_t umask(mode_t mask);
mask 参数为文件的9个访问权限位的按位或。
可以通过设置 umask 控制创建的文件的默认权限。一般在登录时由shell设置一次,以后不再改变它,shell还提供了相应的 umask 命令。
chmod 和 fchmod 函数可以改变现有文件的访问权限。
#include <sys/stat.h> /* 改变文件的访问权限 * 成功返回0,出错返回-1 */ int chmod(const char *path, mode_t mode); int fchmod(int fd, mode_t mode);
参数说明:
- mode
-
除9个文件访问权限位之外,还有6个可取值:
- S_ISUID ,设置用户ID。
- S_ISGID ,设置组ID。
- S_ISVTX ,保存正文(粘住位)。
- S_IRWXU ,用户读、写和执行。
- S_IRWXG ,组读、写和执行。
- S_IRWXO ,其他读、写和执行。
进程必须有超级用户权限或有效用户ID等于文件的用户ID,才能改变该文件的权限位。
粘住位以前是用来使可执行文件执行结束后正文保留在交换区,现在使用虚拟存储器技术后已经不需要这种技术,现在它的含义是:如果目录设置了粘住位,对它有写权限的用户,对于目录下的文件,必须是超级用户或等于目录或该文件的用户才能删除或更名该文件。
文件长度
stat 结构中的 st_size 表示文件的字节数,它只对普通文件、目录文件和符号链接有意义。
普通文件的长度可以是0,目录文件的长度通常是一个数的倍数,符号链接的长度即指向的文件名的字节数。
stat 结构中的 st_blksize 为文件I/O操作的较合适的块长度, st_blocks 为分配的实际512B的块数量。
上一篇提到,文件可能会形成空洞,文件的大小和所使用的磁盘空间不符,空洞部分的字节会被读为0。复制这种文件,会用0填充空洞,使占用的磁盘空间符合文件的大小。
truncate 和 ftruncate 函数可以从文件尾部截短文件。
#include <unistd.h> #include <sys/types.h> /* 将文件截短为length字节,如果原长度小于length,则以0扩展文件 * @return 成功返回0,出错返回-1 */ int truncate(const char *path, off_t length); int ftruncate(int fd, off_t length);
文件时间
文件有三个时间,对应 stat 结构中的 st_atime 、 st_mtime 、 st_ctime ,分别表示文件数据的最后访问时间、文件数据的最后修改时间和i节点状态的最后修改时间。
可以用 utime 和 utimes 更改文件的访问时间和修改时间。
/* 更改文件的访问时间和修改时间 * @return 成功返回0,出错返回-1 */ #include <sys/types.h> #include <utime.h> int utime(const char *filename, const struct utimbuf *times); #include <sys/time.h> int utimes(const char *filename, const struct timeval times[2]);
结构 utimbuf 的定义如下:
struct utimbuf { time_t actime; /* 访问时间 */ time_t modtime; /* 修改时间 */ };
如果 times 参数为 NULL ,则将两个时间都设为当前时间。执行此函数要求进程的有效用户ID等于文件的用户ID,或进程对文件有写权限,如果 times 参数不为 NULL ,则要求进程的有效用户ID等于文件的用户ID,或进程是超级用户进程。
文件系统
接下来总结文件的组织方式,也就是文件系统。磁盘可以分为多个分区,每个分区可以包含一个文件系统。文件系统分成多个柱面组,每个柱面组由i节点、数据块和它们对应的索引、以及一些其他配置组成。
i节点是固定长度的记录项,包含相关文件的大部分信息。每个i节点都有一个链接计数,为指向该i节点的目录项数,当链接计数减至0时,才删除该文件。这种链接称为硬链接, stat 结构中的 st_nlink 给出链接计数。符号链接则是在文件的数据中保存了指向文件的名字,通过i节点中的文件类型可以判断。叶目录的链接计数总为2,即为父目录中的该目录项和该目录中的 . 项,其他目录的链接计数则大于2。
i节点还包含了文件类型、文件访问权限位、文件长度和指向文件占用的数据块的指针等信息。
文件系统对各自的i节点编号,因此目录项不能指向另一个文件系统的i节点,所以硬链接一般不能跨文件系统。在文件系统内部移动文件时,只创建新的i节点更改链接,并不移动数据块。
可以用 link 和 unlink 来创建和删除硬链接。
#include <unistd.h> /* 创建指向现有文件的链接,文件的链接计数加1 * @return 成功返回0,出错返回-1 */ int link(const char *oldpath, const char *newpath); /* 删除链接,文件的链接计数减1 * @return 成功返回0,出错返回-1 */ int unlink(const char *pathname);
一般不允许创建对目录的硬链接,以防止形成循环。
只有在文件的链接计数减至0且没有进程打开它时,它才被内核删除。文件的链接计数为0但当前有进程打开它时,文件不会被删除,直到进程结束后才被删除,常用这种方法来确保临时文件在程序崩溃后不会遗留下来。
C标准中定义了 remove 和 rename 函数。 remove 和对文件的 unlink 、对目录的 rmdir 功能相同。 rename在POSIX.1中被扩展为支持目录和符号链接。
符号链接
引入符号链接是为了避开硬链接的一些限制,可以创建指向目录的符号链接,也可以跨文件系统创建符号链接。
注意对于符号链接,有些函数会处理符号链接指向的文件,有些函数则处理符号链接本身。大多数函数会处理指向的文件,以 l 开头的一些I/O函数、 readlink 、 unlink 、 remove 、 rename 会处理符号链接本身。
symlink 和 readlink 函数创建和打开符号链接。
#include <unistd.h> /* 创建符号链接,注意newpath可以不存在 * @return 成功返回0,出错返回-1 */ int symlink(const char *oldpath, const char *newpath); /* 打开并读取符号链接,然后关闭 * @return 成功返回读到的字节数,出错返回-1 */ ssize_t readlink(const char *path, char *buf, size_t bufsiz);
readlink 返回的内容不以 NULL 字符终止。
目录
用 mkdir 和 rmdir 创建和删除目录。
#include <sys/stat.h> #include <sys/types.h> /* 创建空目录,.和..目录项会自动创建 * @return 成功返回0,出错返回-1 */ int mkdir(const char *pathname, mode_t mode);
#include <unistd.h> /* 删除空目录,空目录是只包含.和..目录项的目录 * @return 成功返回0,出错返回-1 */ int rmdir(const char *pathname);
rmdir 删除 . 和 .. 目录项,使目录的链接计数减为0,如果有进程已经打开该目录,则也不能在目录中创建新文件,在进程结束后删除目录。
只有内核才能写目录,目录的写和执行权限位只决定能否在目录中创建文件和删除文件。
对目录有访问权限的用户可以读目录,目录有特殊的格式,用下面一组函数来读取。
#include <sys/types.h> #include <dirent.h> /* 打开目录文件 * @return 成功返回目录指针,出错返回NULL */ DIR *opendir(const char *name); DIR *fdopendir(int fd); /* 回到目录文件开头 */ void rewinddir(DIR *dirp); /* 关闭目录文件 * @return 成功返回0,出错返回-1 */ int closedir(DIR *dirp); #include <dirent.h> /* 读取目录 * @return 成功返回目录项指针,出错返回NULL */ struct dirent *readdir(DIR *dirp); /* 读取目录的可重入版本,自设缓冲 */ int readdir_r(DIR *dirp, struct dirent *entry, struct dirent **result); /* 获取当前在目录文件中的位置 * @return 返回在目录文件中的当前位置 */ long telldir(DIR *dirp); /* 定位在目录文件中的位置 */ void seekdir(DIR *dirp, long offset);
结构 dirent 的定义为:
struct dirent { ino_t d_ino; /* i节点号 */ off_t d_off; /* 到下个dirent结构的偏移量 */ unsigned short d_reclen; /* 本记录的长度 */ unsigned char d_type; /* 文件类型,不是所有文件系统都支持 */ char d_name[256]; /* 文件名 */ };
ftw 和 nftw 函数遍历文件目录树,可用上面的目录操作函数自行实现它们。
进程有一个当前的工作目录,它是搜索相对路径名的起点。可以用 chdir 和 fchdir 函数更改进程的当前工作目录。
#include <unistd.h> /* 更改进程的当前工作目录 * @return 成功返回0,出错返回-1 */ int chdir(const char *path); int fchdir(int fd);
getcwd 函数获取进程的当前工作目录。
#include <unistd.h> /* 获取进程的当前工作目录 * @return 成功返回buf,出错返回NULL */ char *getcwd(char *buf, size_t size);
buf 和 size 参数分别为缓冲的地址和长度,长度需要加上 NULL 终止符的1字节。
经常会遇到需要切换工作目录,完成处理后再切换回来。有一个技巧是用 open 打开当前工作目录,保存文件描述符,完成处理后再把它传给 fchdir 函数,这比使用 getcwd 方便一些。
设备特殊文件
存储设备一般由主、次设备号表示,通常一个磁盘上的不同文件系统有相同的主设备号和不同的次设备号,它们保存在 stat 结构中的 st_dev 中,可以用 major 和 minor 函数来访问它们。
#include <sys/types.h> /* 读取主、次设备号 */ int major(dev_t dev); int minor(dev_t dev);
块特殊文件和字符特殊文件才有 st_rdev 值,它包含实际设备的设备号。
用户
Linux是多用户系统,用户的信息保存在口令文件和组文件中,在登录和其他需要获取用户信息的情况下都要使用它们。
口令文件
口令文件保存在 /etc/passwd 中,编程中使用 passwd 结构保存每一项:
struct passwd { char *pw_name; /* 用户名 */ char *pw_passwd; /* 用户密码 */ uid_t pw_uid; /* 用户ID */ gid_t pw_gid; /* 组ID */ char *pw_gecos; /* 用户信息 */ char *pw_dir; /* 工作目录 */ char *pw_shell; /* shell */ };
可以用 getpwnam 和 getpwuid 来查询相关项。
#include <sys/types.h> #include <pwd.h> /* 获取口令文件项 * @return 成功返回passwd结构的指针,出错返回NULL */ struct passwd *getpwnam(const char *name); struct passwd *getpwuid(uid_t uid); /* 获取口令文件项的可重入版本,自设缓冲 */ int getpwnam_r(const char *name, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result); int getpwuid_r(uid_t uid, struct passwd *pwd, char *buf, size_t buflen, struct passwd **result);
如果需要查看整个口令文件,可以使用 getpwent 函数族。注意最后一定要用 endpwent 关闭口令文件。
#include <sys/types.h> #include <pwd.h> /* 获取口令文件中的下一项 * @return 成功返回passwd结构的指针,出错返回NULL */ struct passwd *getpwent(void); /* 回到口令文件的开头 */ void setpwent(void); /* 关闭口令文件 */ void endpwent(void);
现在通常将密码保存在阴影口令文件中,即 /etc/shadow ,一般用户无法读取它。将它和口令文件分开以提高安全性。
阴影口令文件中的项也有对应的结构 spwd :
struct spwd { char *sp_namp; /* 登录名 */ char *sp_pwdp; /* 加密密码 */ long sp_lstchg; /* 上次更改密码后经过的天数,自1970-01-01 */ long sp_min; /* 多少天后可更改 */ long sp_max; /* 多少天内必须更改 */ long sp_warn; /* 到期前警告天数 */ long sp_inact; /* 到期后停用前天数 */ long sp_expire; /* 账户到期的天数,自1970-01-01 */ unsigned long sp_flag; /* 保留 */ };
其中 sp_namp 和 sp_pwdp 是必需字段。
也有一组访问阴影口令文件的函数。
#include <shadow.h> /* 获取阴影口令文件项 * @return 成功返回spwd结构的指针,出错返回NULL */ struct spwd *getspnam(const char *name); struct spwd *getspent(void); /* 回到阴影口令文件的开头 */ void setspent(void); /* 关闭阴影口令文件 */ void endspent(void);
组文件
组文件保存在``/etc/group`` 中,编程中使用 group 结构保存每一项:
struct group { char *gr_name; /* 组名 */ char *gr_passwd; /* 组密码 */ gid_t gr_gid; /* 组ID */ char **gr_mem; /* 组成员 */ };
和口令文件的相关函数类似,可以用 getgrnam 和 getgrgid 查询相关项。 getgrent 函数族可以查看整个组文件。
#include <sys/types.h> #include <grp.h> /* 获取组文件项 * @return 成功返回group结构的指针,出错返回NULL */ struct group *getgrnam(const char *name); struct group *getgrgid(gid_t gid); /* 获取组文件项的可重入版本,自设缓冲 */ int getgrnam_r(const char *name, struct group *grp, char *buf, size_t buflen, struct group **result); int getgrgid_r(gid_t gid, struct group *grp, char *buf, size_t buflen, struct group **result); /* 获取组文件中的下一项 * @return 成功返回指向group结构的指针,出错返回NULL */ struct group *getgrent(void); /* 回到组文件的开头 */ void setgrent(void); /* 关闭组文件 */ void endgrent(void);
附加组
用户属于口令文件项中组ID对应的组,同时也可以属于其他的组。在检查文件访问权限时,会将进程的有效组ID和所有附加组ID都与文件的组ID比较。
#include <sys/types.h> #include <unistd.h> #include <grp.h> /* 将附加组ID写入list中,list的长度为size * @return 成功返回附加组ID数,出错返回-1 */ int getgroups(int size, gid_t list[]); /* 为进程设置附加组ID表,由超级用户调用(一般由initgroups),list中附加组ID数为size * @return 成功返回0,出错返回-1 */ int setgroups(size_t size, const gid_t *list); /* 读取组文件,为user初始化附加组ID表,由超级用户调用,group为user的组ID * @return 成功返回0,出错返回-1 */ int initgroups(const char *user, gid_t group);
用户登录名
一个用户可能有多个登录名,如果想获取用户的当前登录名,可以用 getlogin 函数。
#include <unistd.h> /* 获取当前登录名 * @return 成功返回指向登录名字符串的指针,出错返回NULL */ char *getlogin(void); /* 获取当前登录名的可重入版本,自设缓冲 */ int getlogin_r(char *buf, size_t bufsize);
系统标识
uname 函数返回当前主机和操作系统的有关信息。 uname 命令使用它打印这些信息。
#include <sys/utsname.h> /* 获取主机和系统信息 * @return 成功返回0,出错返回-1 */ int uname(struct utsname *buf);
使用该函数时,向其传递指向 utsname 结构的地址,由函数写入。该结构的定义如下:
struct utsname { char sysname[]; /* 操作系统名称 */ char nodename[]; /* 节点名称 */ char release[]; /* 操作系统发行版 */ char version[]; /* 操作系统版本 */ char machine[]; /* 硬件标识 */ };
gethostname 函数返回主机名,即TCP/IP网络上的主机的名字。 hostname 命令可用来获取和设置主机名。
#include <unistd.h> /* 获取和设置主机名 * @return 成功返回0,出错返回-1 */ int gethostname(char *name, size_t len); int sethostname(const char *name, size_t len);
日期和时间
Linux中除C标准的日期和时间函数之外,还提供了更高精度的 gettimeofday 函数,它能精确到微秒量级。
#include <sys/time.h> /* 获取和设置时间 * @return 成功返回0,出错返回-1 */ int gettimeofday(struct timeval *tv, struct timezone *tz); int settimeofday(const struct timeval *tv, const struct timezone *tz);
其中的 timeval 和 timezone 结构定义如下:
struct timeval { time_t tv_sec; /* 秒 */ suseconds_t tv_usec; /* 微秒 */ }; struct timezone { int tz_minuteswest; /* 格林尼治时间以西分钟数 */ int tz_dsttime; /* 夏令时修正类型 */ };