0%

软链接(符号链接)

软链接本质上存储了目标文件名,但因为是特殊的文件,我们不能直接用 openat 打开软链接文件并用 read 读取其内容。想要跟踪符号链接,可以向 readlink 系统调用传入软链接文件路径。

openat 尝试读取软链接目标有什么错误?

openat 默认会 follow 符号链接,这不是我们期望的;如果把 flags 设置成 O_RDONLY | O_NOFOLLOW,则会(在遇到软链接文件时)直接打开失败。

Important

不像 openatlstat 和带有 AT_SYMLINK_NOFOLLOW 标志的 fstatat 就能够读取符号链接本身的信息。

Note

AT_SYMLINK_NOFOLLOWAT_SYMLINK_FOLLOW 两个宏都是有定义的,而且它们处在不同的位!

似乎没有通过软链接本身的 fd 获得软链接目标的系统调用?给 openat 传入 O_PATH | O_NOFOLLOW 的确可以得到一个不会跟踪的、没有打开文件的、只能标识文件位置的 fd,但是获取这样的 fd 就需要文件路径作为参数,所以可能没有必要设计这样的一个系统调用?

用 chown 修改 owner

Linux 和 BSD 下只有 root 才可以。有些系统允许用户修改自己文件的所有者完成文件转让

用 chown 修改 group

超级用户肯定是可以的。

或者,进程拥有此文件(看有效用户 ID),参数 owner 等于 -1 或者文件所有者的 ID,参数 group 等于进程的有效组 ID 或者附属组 ID 中的一个。

这意味着非超级用户只能更改自己拥有文件的组 ID,而且只能改到自己所属的组(即完成文件的组内转让)。

int truncate(const char *path, off_t length);
int ftruncate(int fd, off_t length);

这两个函数能够设置文件长度,如果新的长度比原来更短,则旧的内容被截断。如果新的长度比原来更长,则中间的部分会形成空洞。形成空洞的部分在被其他程序读取的时候,得到的是 0;全 0 的块可以不实际分配给文件,因此不会增加文件体积。

umask 系统调用

这样理解屏蔽字:当某一位被设置为 1,这一位就不起作用。umask 把当前进程的文件创建屏蔽字设置为 mask & 0777,也就是只有文件权限位是受到屏蔽的。umask 能影响 openmkdir 等系统调用,这相当于每次传入它们的 mode_t 参数都用 umask 屏蔽了给定的位。

Tip

  • 文件打开标志中int flags),读、写、读写三个状态被设置成了互斥的,读和写不能简单叠加得到读写。它们的宏以 O_ 开头(可能表示 open),分别是 O_RDONLY / O_WRONLY / O_RDWR,访问前要先用 O_ACCMODE 掩码对 flags 做好处理。
  • 文件访问权限模式mode_t mode)读、写、执行可以叠加。它们的宏以 S_I 开头(可能表示 stat、inode),然后跟 R/W/X 中的一个,再跟 USR/GRP/OTH 中的一个。

Tip

掩码和屏蔽字好像都是 mask。前者的用法是 x & mask,后者的用法是 x & ~mask

默认的 umask 值是 S_IWGRP | S_IWOTH,也就是 022,这表示如果无其他修改,我们无法简单地创建 group 和 others 可以写入的文件。

当文件父目录被设置了 ACL 时,umask 被忽略。

区别于 umask 的操作只作用于文件的读写执行相关的 9 位,chmod 是字如其名的操作 struct stat 类型 st_mode 字段的系统调用。

chmod 系统调用只会改变文件 inode 结点的上次修改时间,不会改变文件内容的上次修改时间。因此,用 ls 列出 chmod 修改过权限的文件属性时,我们不会看到文件的上次修改时间有变化。

$ ls -lh A.txt
-rw-r----- 1 root root 20 May  8 23:35 A.txt
$ chmod 644 A.txt
$ ls -lh A.txt
-rw-r--r-- 1 root root 20 May  8 23:35 A.txt

chmod 在一些条件下会清除粘着位、设置用户 ID 位和设置组 ID 位。略。

粘着(sticky)位对应 S_ISVTX(saved-text bit,又叫保存正文位),在尚未使用请求分页的早期 UNIX 版本中,带有粘着位的可执行文件在第一次执行结束之后其代码副本会被保存到交换区,这样下一次就可以被更快被加入到内存中。常用的文本编辑器或者编译器都适合使用粘着位。

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

void
perror_and_exit (const char *msg)
{
    int err = errno;
    fflush (stdout);
    fprintf (stderr, "%s: %s\n", msg, strerror (err));
    exit (errno);
}

int
main (int argc, char **argv)
{
    if (argc != 2)
        {
            fprintf (stderr, "Usage: %s <path>\n", argv[0]);
            exit (1);
        }
    struct stat buf;
    if (stat (argv[1], &buf) == -1)
        perror_and_exit ("stat");
    if (buf.st_mode & S_ISVTX)
        printf ("S_ISVTX is set\n");
    else
        printf ("The bit is not set\n");
}

编译运行:

⇨ /data/apue $ /data/apue/build/apue build/
The bit is not set
⇨ /data/apue $ /data/apue/build/apue /tmp/
S_ISVTX is set
⇨ /data/apue $ /data/apue/build/apue /var/
The bit is not set
⇨ /data/apue $ /data/apue/build/apue /var/tmp/
S_ISVTX is set

验证一下没有粘着位的影响:

⇨ /data/apue $ sudo touch build/rootfile.txt
[sudo] password for <USER>: 
⇨ /data/apue $ ls -lAh build/rootfile.txt 
-rw-r--r-- 1 root root 0 May 12 22:18 build/rootfile.txt
⇨ /data/apue $ mv build/rootfile.txt build/userfile.txt
⇨ /data/apue $ 

进程有哪些用户或组 id?

和一个进程关联的 id 有 6 个或者更多:

  • 实际用户 id 和实际组 id
  • 有效用户 id、有效组 id 和附属组 id
  • 保存的设置用户 id 和保存的设置组 id(由 exec 函数保存)

有效用户(组)决定实际访问权限

什么是有效用户 id?

一个进程在启动时可以以别人的身份启动,从而拥有别人的权限,这个时候真正生效的是有效用户 id。有效组 id 也是类似的。

什么时候需要用别人的身份启动?

passwd 程序就是不需要 root 权限的,但是能够修改 root 才能写入的文件,这是因为 passwd 程序就使用了 root 作为有效用户。其他例子包括:mountunmountsuwall,前三个是设置用户 ID,最后一个是设置组 ID,其作用是向 tty 下辖的所有终端写入一条信息。

如何按文件设置有效用户 id?

可以在文件的模式字(st_mode)中设置特殊标志,用来表示执行此文件时,将进程的有效用户 id 设置为文件所有者的用户 id,这个特殊标志就是设置用户 id 位。同样,还有设置组 id 位

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>

void
perror_and_exit (const char *msg)
{
    int err = errno;
    fflush (stdout);
    fprintf (stderr, "%s: %s\n", msg, strerror (err));
    exit (errno);
}

int
main (int argc, char **argv)
{
    struct stat buf;
    const char *ptr;
    for (int i = 1; i < argc; ++i)
        {
            printf ("%s ", argv[i]);
            if (lstat (argv[i], &buf) < 0)
                perror_and_exit ("lstat");
            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";
            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);
        }
}

例子:

$ /data/apue/build/apue /home /dev/fd/0 /
/home directory
/dev/fd/0 symbolic link
/ directory

抄的书上的代码。

#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/param.h>
#include <sys/syscall.h>
#include <sys/times.h>
#include <unistd.h>

extern int errno;

void
perror_and_exit (const char *msg)
{
    perror (msg);
    exit (errno);
}

int
main (int argc, char **argv)
{
    int val;
    if (argc != 2)
        {
            fputs ("usage: a.out <descriptor#>\n", stderr);
            exit (1);
        }
    if ((val = fcntl (atoi (argv[1]), F_GETFL, 0)) < 0)
        {
            int ec = errno;
            fprintf (stderr, "fcntl error for fd %d: %s", atoi (argv[1]),
                     strerror (ec));
            exit (ec);
        }
    switch (val & O_ACCMODE)
        {
        case O_RDONLY:
            printf ("read only");
            break;
        case O_WRONLY:
            printf ("write only");
            break;
        case O_RDWR:
            printf ("read write");
            break;
        default:
            perror_and_exit ("unknown access mode");
        }
    if (val & O_APPEND)
        printf (", append");
    if (val & O_NONBLOCK)
        printf (", nonblocking");
    if (val & O_SYNC)
        printf (", synchronous writes");
#if !defined(_POSIX_C_SOURCE) && defined(O_FSYNC) && (O_FSYNC != O_SYNC)
    if (val & O_FSYNC)
        printf (", synchronous writes");
#endif
    putchar ('\n');
    exit (0);
}

非常惊讶的是,Linux 上 Bash 给应用程序分配的标准输入、输出、错误流都是可读可写的。似乎 Bash 是把当前的打字机设备(tty)打开并分配给了应用程序的三个标准流,因为 /proc/<pid>/fd/0 (和其他两个)都是链接到了 /dev/pts/5 上,而这个设备是我当前的终端。


$ /data/apue/build/apue 0
read write
$ /data/apue/build/apue 1
read write
$ /data/apue/build/apue 2
read write
$ /data/apue/build/apue 0 <A.txt
read only
$ /data/apue/build/apue 0 <>A.txt
read write
$ /data/apue/build/apue 5 5<>B.txt
read write

注意 Bash 中 /data/apue/build/apue 5 5<>B.txt 这种重定向,它把 5 号文件描述符分配给了 B.txt 文件,而且是用可写的方式打开。因为这里有写方式打开,所以 B.txt 不存在时会自动创建,如果是只读方式打开,那么 B.txt 不存在就会报错。