目录

[单刷APUE系列]第一章——Unix基础知识[1]
[单刷APUE系列]第一章——Unix基础知识[2]
[单刷APUE系列]第二章——Unix标准及实现
[单刷APUE系列]第三章——文件I/O
[单刷APUE系列]第四章——文件和目录[1]
[单刷APUE系列]第四章——文件和目录[2]
[单刷APUE系列]第五章——标准I/O库
[单刷APUE系列]第六章——系统数据文件和信息
[单刷APUE系列]第七章——进程环境
[单刷APUE系列]第八章——进程控制[1]
[单刷APUE系列]第八章——进程控制[2]
[单刷APUE系列]第九章——进程关系
[单刷APUE系列]第十章——信号[1]

stat函数族

int stat(const char *restrict path, struct stat *restrict buf);
int fstat(int fildes, struct stat *buf);
int lstat(const char *restrict path, struct stat *restrict buf);
int fstatat(int fd, const char *path, struct stat *buf, int flag);

The stat() function obtains information about the file pointed to by path.  Read, write or execute permission of the named file is not required, but all directories listed in the path name leading to the file must be searchable.

The lstat() function is like stat() except in the case where the named file is a symbolic link; lstat() returns information about the link, while stat() returns information about the file the link references.  The attributes cannot be relied on in case of symbolic links.  In this case, the only attributes returned from an lstat() that refer to the symbolic link itself are the file type (S_IFLNK), size, blocks, and link count (always 1).

The fstat() obtains the same information about an open file known by the file descriptor fildes.

The fstatat() system call is equivalent to stat() and lstat() except in the case where the path specifies a relative path.  In this case the status is retrieved from a file relative to the directory associated with the file descriptor fd instead of the current work-ing directory.

stat()函数获得路径所指向的文件信息,函数不需要文件的读写执行权限,但是路径树中的所有目录都需要搜索权限(search)
lstat()函数和stat类似,除了当路径指向为符号链接时,lstat返回链接本身信息,而stat返回对应的文件信息
fstat()获取已经打开的文件描述符的文件信息
fstatat()系统调用等价于stat和lstat函数,当AT_FDCWD传入fd参数,并且路径参数为相对路径时,将会计算基于当前工作目录的文件,如果路径为绝对路径,则fd参数被忽略,这个函数实际上是前面几个函数的功能集合版本。
第二个参数buf则是一个结构体指针,原著中定义的版本

struct stat {
    mode_t st_mode;
    ino_t st_ino;
    dev_t st_dev;
    nlink_t st_nlink;
    uid_t st_uid;
    gid_t st_gid;
    off_t st_size;
    struct timespec st_atime;
    struct timespec st_mtime;
    struct timespec st_ctime;
    blksize_t st_blksize;
    blkcnt_t st_blocks;
}

实际上各个Unix环境都扩充了标准外的实现,以Mac OS X为例
当未定义_DARWIN_FEATURE_64_BIT_INODE宏定义

struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is NOT defined */
    dev_t    st_dev;    /* device inode resides on */
    ino_t    st_ino;    /* inode's number */
    mode_t   st_mode;   /* inode protection mode */
    nlink_t  st_nlink;  /* number of hard links to the file */
    uid_t    st_uid;    /* user-id of owner */
    gid_t    st_gid;    /* group-id of owner */
    dev_t    st_rdev;   /* device type, for special file inode */
    struct timespec st_atimespec;  /* time of last access */
    struct timespec st_mtimespec;  /* time of last data modification */
    struct timespec st_ctimespec;  /* time of last file status change */
    off_t    st_size;   /* file size, in bytes */
    quad_t   st_blocks; /* blocks allocated for file */
    u_long   st_blksize;/* optimal file sys I/O ops blocksize */
    u_long   st_flags;  /* user defined flags for file */
    u_long   st_gen;    /* file generation number */
};

_DARWIN_FEATURE_64_BIT_INODE被定义后

struct stat { /* when _DARWIN_FEATURE_64_BIT_INODE is defined */
    dev_t           st_dev;           /* ID of device containing file */
    mode_t          st_mode;          /* Mode of file (see below) */
    nlink_t         st_nlink;         /* Number of hard links */
    ino_t           st_ino;           /* File serial number */
    uid_t           st_uid;           /* User ID of the file */
    gid_t           st_gid;           /* Group ID of the file */
    dev_t           st_rdev;          /* Device ID */
    struct timespec st_atimespec;     /* time of last access */
    struct timespec st_mtimespec;     /* time of last data modification */
    struct timespec st_ctimespec;     /* time of last status change */
    struct timespec st_birthtimespec; /* time of file creation(birth) */
    off_t           st_size;          /* file size, in bytes */
    blkcnt_t        st_blocks;        /* blocks allocated for file */
    blksize_t       st_blksize;       /* optimal blocksize for I/O */
    uint32_t        st_flags;         /* user defined flags for file */
    uint32_t        st_gen;           /* file generation number */
    int32_t         st_lspare;        /* RESERVED: DO NOT USE! */
    int64_t         st_qspare[2];     /* RESERVED: DO NOT USE! */
};

一般这些类型都是基本数据类型,而timespec则是一个结构体,包含了纳秒和秒

_STRUCT_TIMESPEC
{
        __darwin_time_t tv_sec;
        long            tv_nsec;
};

文件类型

Unix系统中所有东西都是文件,这就是Unix的哲学,一切皆是文件。文件类型有以下几种

  1. 普通文件

  2. 目录文件

  3. 块特殊文件

  4. 字符特殊文件

  5. FIFO

  6. 套接字

  7. 符号链接

普通文件包含了文本文件和二进制文件,想要让二进制文件可执行,文件必须按照内核规定的格式防止各个段的数据。
FIFO主要用于进程间通信,套接字则是一个非常重要的文件,用于进程间的网络通信,具体可以分为好几种

#define S_ISBLK(m)      (((m) & S_IFMT) == S_IFBLK)     /* block special */
#define S_ISCHR(m)      (((m) & S_IFMT) == S_IFCHR)     /* char special */
#define S_ISDIR(m)      (((m) & S_IFMT) == S_IFDIR)     /* directory */
#define S_ISFIFO(m)     (((m) & S_IFMT) == S_IFIFO)     /* fifo or socket */
#define S_ISREG(m)      (((m) & S_IFMT) == S_IFREG)     /* regular file */
#define S_ISLNK(m)      (((m) & S_IFMT) == S_IFLNK)     /* symbolic link */
#define S_ISSOCK(m)     (((m) & S_IFMT) == S_IFSOCK)    /* socket */
#if !defined(_POSIX_C_SOURCE) || defined(_DARWIN_C_SOURCE)
#define S_ISWHT(m)      (((m) & S_IFMT) == S_IFWHT)     /* OBSOLETE: whiteout */
#endif

<sys/stat.h>中定义了一堆宏来帮助确定文件类型,这些宏用于判断st_mode的类型,上面是Mac OS X的头文件,相对于POSIX规定多了一项。

#include "include/apue.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) {
            err_ret("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);
}

这就是一个例程,用于判断文件的类型,这里使用了lstat函数而不是stat函数以便于我们观察到符号链接。

用户组ID和组ID

对于一个存在于磁盘上的文件来说,它只有一个拥有者uid和拥有者gid,分别被保存在stat结构体的st_uidst_gid中,但是对于进程来说关联的ID有6个或者更多,原著中提到了6个ID

各个ID的名称 代表的含义
实际用户ID和实际组ID 我们实际上是谁
有效用户ID、有效组ID和附属组ID 用于文件访问权限检查
保存的设置用户ID和保存的设置组ID 由exec函数保存

可能在看原著的时候,有很多人看不懂这里到底讲的是什么。一些做过Node.JS开发的人可能知道,Node是一个单进程的模型,并且自身就包含了Web服务器的工作,Unix系统规定,1024以下的端口只有拥有root权限才能打开,而且为了保证进程以非root权限执行。所以Node想要在80端口服务就只有三种办法,第一种就是以root权限运行,然后使用process更改当前进程的uid和gid,第二种是使用非root权限运行Node,再高位端口打开侦听,然后使用Nginx反向代理80端口,第三种就是实现master和worker进程,让Node以多进程的方式运行。
对于开发者来说,有效用户ID和有效组ID才是最重要的,因为它们才包含了进程可访问的权限,包括各种文件、端口的打开,等等资源的使用都是依赖有效用户ID和有效组ID,实际用户ID和实际组ID则是继承于shell的会话用户,因为是shell启动的进程。
至于保存的设置用户ID和保存的设置组ID则是用于exec派生子进程的使用交付给子进程的实际用户ID和实际组ID。
通常来说,有效用户ID等于实际用户ID,有效组ID等于实际组ID。
学过Unix文件权限的朋友应该知道,除了rwx权限以外,还有s权限,这就是设置用户ID和设置组ID,它能让进程有效用户ID和有效组ID等于程序拥有者的uid和gid,例如sudopasswd命令

文件的访问权限

st_mode成员还包含了文件的访问权限位。这个东西可能很多朋友都知道,分别是用户rwx、组rwx和其他rwx,可以使用chmod来改变它们。
文件访问权限有9种

st_mode屏蔽 含义
S_IRUSR 用户读
S_IWUSR 用户写
S_IXUSR 用户执行
S_IRGRP 组读
S_IWGRP 组写
S_IXGRP 组执行
S_IROTH 其他读
S_IWOTH 其他写
S_IXOTH 其他执行

新文件和目录的所有权

原著已经讲得很详细了,这里就不再赘述。

访问权限测试函数族

access, faccessat -- check access permissions of a file or pathname

int access(const char *path, int amode);
int faccessat(int fd, const char *path, int mode, int flag);

The real user ID is used in place of the effective user ID and the real group access list (including the real group ID) are used in place of the effective ID for verifying permission.

这两个函数测试的是实际用户ID和实际组ID,faccessat函数更加强大,既可以测试实际用户ID,也可以测试有效用户ID,除非是少数情况,大部分都是使用faccessat函数

#include "include/apue.h"
#include <fcntl.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);
}

在例程中,使用了两个函数accessopen分别测试实际用户ID、有效用户ID对于进程的影响,我们可以将程序的执行权限设置为s,然后将所有者换成root,这样就能测试到进程实际用户ID和有效用户ID不同带来的影响了。

umask函数

mode_t umask(mode_t cmask);

The default mask value is S_IWGRP | S_IWOTH (022, write access for the owner only).  Child processes inherit the mask of the calling process.

函数是为进程设置屏蔽字,并且返回原先的值,当cmask中指定某位的bit被打开,那么文件模式中的相应位奖杯关闭。
Unix环境中系统都提供了一个shell命令umask,用于显示和设置文件模式创建掩码字,实际上是使用了umask函数,其中cmask参数就是前面st_mode的9个参数按位OR运算得到。默认情况下,掩码字是022,也就是说去除了组写和其他写权限。

#include "include/apue.h"
#include <fcntl.h>

#define RWRWRW (S_IRUSR|S_IWUSR|S_IRGRP|S_IWGRP|S_IROTH|S_IWOTH)

int main(int argc, char *argv[])
{
    umask(0);
    if (creat("foo", RWRWRW) < 0)
        err_sys("creat error for foo");
    umask(S_IRGRP | S_IWGRP | S_IROTH | S_IWOTH);
    if (creat("bar", RWRWRW) < 0)
        err_sys("creat error for bar");
    exit(0);
}

然后运行

> umask
022
> ./a.out
> ls -l foo bar
-rw-------  1 uid  gid  0  2  1 15:18 bar
-rw-rw-rw-  1 uid  gid  0  2  1 15:18 foo
> umask
022

这样应该很清楚了,对于shell来说,他会读取启动文件的设置从而在启动时自行设置为默认的掩码字,在运行过程中,用户可以使用umask命令手动更改掩码字,shell打开一个进程,新的进程则会继承shell的umask值,在使用Unix的时候,经常需要使用的mkdir、touch之类的创建文件的命令,这些命令在运行的时候就是继承shell的umask,在前面的实例中我们看到,子进程中设置umask掩码字完全没有影响到父进程。
原著中写的一些东西非常让人费解,实际上是这样的,原著中少讲了一点东西,那就是opencreat等函数第三个mode参数实际上不是所需要创建文件的权限,而是要通过运算的,最终权限是这么得出的

mode & (~cmask)

还记得前面讲过的clr_fl函数吗,这里也是同样的,取反然后进行AND运算,当我们使用mkdir等命令创建文件文件夹的时候,实际上第三个参数是rwxrwxrwx或者rwrwrw,然后通过umask运算得到最终的权限。
而且还有一个非常有意思的东西,当我们使用creat函数创建文件,并且第三个参数不指定的时候,编译时会报错过少的参数,但是当我们使用open函数创建文件的时候,不指定第三个参数照样是可以创建的,只是第三个参数会被默认指定为0,也就是没有任何权限。
所以,当我们在做开发的时候,如果想要保证自己能完全指定文件的权限,那么必须在运行时使用umask函数修改为0,否则非0得umask值可能会关闭我们需要的权限位置,当然,如果进程不需要关心文件的权限问题,那么完全可以指定rwxrwxrwx或者rwrwrw的权限,然后umask会自动根据默认值将其修改。

文件权限修改函数族

int chmod(const char *path, mode_t mode);
int fchmod(int fildes, mode_t mode);
int chmodat(int fd, const char *path, mode_t mode, int flag);

第三个chmodat函数多了一个flag参数以外,其余和chmod函数都是一样的。正如前面的一些函数,flag参数只有一个参数可用AT_SYMLINK_NOFOLLOW,用于确认是否跟随链接。当flag为0的时候,和其他两个函数等价。

#define S_IRWXU 0000700    /* RWX mask for owner */
#define S_IRUSR 0000400    /* R for owner */
#define S_IWUSR 0000200    /* W for owner */
#define S_IXUSR 0000100    /* X for owner */

#define S_IRWXG 0000070    /* RWX mask for group */
#define S_IRGRP 0000040    /* R for group */
#define S_IWGRP 0000020    /* W for group */
#define S_IXGRP 0000010    /* X for group */

#define S_IRWXO 0000007    /* RWX mask for other */
#define S_IROTH 0000004    /* R for other */
#define S_IWOTH 0000002    /* W for other */
#define S_IXOTH 0000001    /* X for other */

#define S_ISUID 0004000    /* set user id on execution */
#define S_ISGID 0002000    /* set group id on execution */
#define S_ISVTX 0001000    /* save swapped text even after use */

是否感觉非常的眼熟,其实就是前面9个文件访问权限加上S_ISUIDS_ISGID两个设置ID常量,也就是s位权限,S_ISVTX保存正文常量,以及三个组合常量S_IRWXUS_IRWXGS_IRWXO

#include "include/apue.h"

int main(int argc, char *argv[])
{
    struct stat statbuf;
    if (stat("foo", &statbuf) < 0)
        err_sys("stat error for foo");
    if (chmod("foo", (statbuf.st_mode & ~S_IXGRP) | S_ISGID) < 0)
        err_sys("chmod error for foo");
    if (chmod("bar", S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH) < 0)
        err_sys("chmod error for bar");
    exit(0);
}

前面一个例程创建了foo和bar文件,并且设置了权限位,当运行上面的程序,这两个最后状态如下

> ls -l foo bar
-rw-r--r--  1 chasontang  staff  0  2  1 18:18 bar
-rw-rwSrw-  1 chasontang  staff  0  2  1 18:18 foo

大写的S表示只设置了设置组ID但未设置执行ID。
顺便说一句,如果这种特殊权限位没有设置执行权限,实际上是根本没有任何作用的。


忘记讲一点了,通过查看man 2 chmod的说明,可以看到一段话

Writing or changing the owner of a file turns off the set-user-id and set-group-id bits unless the user is the super-user.  This makes the system somewhat more secure by protecting set-user-id (set-group-id) files from remaining set-user-id (set-group-id) if they are modified, at the expense of a degree of compatibility.

当写入或者改变文件的拥有者会自动导致setuid和setgid位被自动清除,这是为了防止设置用户ID和设置组ID被滥用,如果有恶意程序修改了带有这两个参数的程序,就会被清除这两个参数,防止被提权。


山河永寂
2.4k 声望159 粉丝