0%

Linux 进程凭证

进程凭证包括以下内容:

  • 实际用户 ID(real user ID)和实际组 ID(real group ID)。
  • 有效用户 ID(effective user ID)和有效组 ID(effective group ID)。
  • 保存的 set-user-ID(saved set-user-ID)和保存的 set-group-ID(saved set-group-ID)。
  • 文件系统用户 ID(file-system user ID)和文件系统组 ID(file-system group ID)(Linux 专有)。
  • 辅助组 ID。

保存的设置用户/组 ID

文件系统中每个非目录文件有设置用户/组 ID 位,启动这样的文件会使得有效用户/组 ID 按照文件的所有者来设置。出于安全考虑,在 Linux 中可执行文件的设置用户/组 ID 权限对于 shell 脚本无效

设计保存的设置用户 ID(保存的设置组 ID 同,为了表述方便略去)是为了让程序的有效用户 ID 能够在实际用户 ID保存的设置用户 ID 之间切换。这样程序在不需要使用其他用户(尤其是 root)的权限时可以将有效用户 ID 转回实际用户 ID,需要权限时再转回来。程序启动时,保存的设置用户 ID 从有效用户 ID 复制过来。

文件系统用户/组 ID

文件系统用户/组 ID 管辖和文件系统操作相关的权限问题,是由 Linux 专有的。一般情况下它们和有效用户/组 ID 相同,除非调用了 setfsuid / setfsgid

在 Linux 上推荐使用的创建临时文件的方法只有 mkstemptmpfile。前者是系统调用,用起来更复杂一点,后者是 C 标准库函数。其他的函数多多少少有自己的问题。

tmpfile 不再需要我们提供字符数组来存储文件名,返回的是 FILE* 而不是 fd。此外还在刚创建文件之后就使用了 unlink 来删除文件,这样就只有当前进程可以访问到这个文件了(除非 fork)。

#include <assert.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    // 创建文件,记得关闭。
    FILE *fp;
    assert((fp = tmpfile()) != NULL);

    // 查询文件路径并打印。
    char path[128];
    char link_path[128];
    int fd = fileno(fp);
    assert(fd != -1);    
    sprintf(link_path, "/proc/self/fd/%d", fd);
    assert(readlink(link_path, path, sizeof(path)) != -1);
    printf("tmpfile: %s\n", path);

    fprintf(fp, "write something\n");
    fflush(fp);

    // 在这里停顿。由于已经 unlink 了这个文件,文件系统上看不到它。
    getchar();

    fclose(fp);
}

运行结果:

(py310) xxx /data/apue $ /data/apue/build/apue
tmpfile: /tmp/#943520 (deleted)

/proc/self/cmdline 是一个以 \0 结尾的文件,我尝试过用 fgets 读取,但是失败了;转而使用 fgetc 逐字符读文件。

#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>

int main() {
    FILE *f = fopen("/proc/self/cmdline", "r");
    if (!f) {
        perror("fopen");
        return 1;
    }
    
    int i = 0, j = 0;
    int ch;
    char buf[128];
    while ((ch = fgetc(f)) != EOF) {
        if (ch == '\0') {
            buf[j] = '\0';
            printf("argv[%d]: %s\n", i++, buf);
            j = 0;
        } else {
            buf[j++] = ch;
        }
    }

    fclose(f);
}

例子:

(py310) xxx /data/apue $ /data/apue/build/apue 5 265
argv[0]: /data/apue/build/apue
argv[1]: 5
argv[2]: 265

这被称为 Scatter-Gather I/O。

其中,struct iovec 的定义如下:

struct iovec {
  void  *iov_base; /* Start address of the buffer */
  size_t iov_len;  /* Number of bytes to transfer to/from buffer */
};

这样 readvwritev 就能一次完成多个缓冲区的读取/写入。最重要的特征是:和多次调用 read/write 相比, readv/writev 操作具有原子性!

对书的介绍

Linux/UNIX 系统编程手册原名 The Linux Programming Interface,副标题是 A Linux and UNIX System Programming Handbook,这也是其翻译名的由来。

这本书经常被简称为 TLPI。

笔记的编号方式

有些笔记的编号是在章内简单排序(在书上没有对应或者不能直接对应),有些笔记的编号是和书的章节对应的。这点确实做的不好,看书的时间太长了,写笔记的方式也发生了变化。

我的测试环境

有一部分是在本机的 wsl 测试的,有一部分是在服务器上测试的(会明确说明)。

2024 年 7 月 20 日:开始考虑使用虚拟机安装系统测试,对应内容是 41 共享库基础 的中间部分(及以后)。

#include <errno.h>
#include <fcntl.h>
#include <linux/kcmp.h>
#include <stdio.h>
#include <sys/syscall.h>
#include <unistd.h>

int main() {
    pid_t pid = getpid(); /* always successful */
    int ret = syscall(SYS_kcmp, pid, pid, KCMP_FILE, 0, 1);
    int ec = errno;
    if (ec) {
        perror("kcmp");
        return 1;
    }
    printf("kcmp returns %d\n", ret);
}

其中,kcmp 系统调用可以用来比较两个进程的资源大小(0 等于,1 小于,2 等于,3 不相等但无法确定大小)。输出:

kcmp returns 0

这说明此文件在终端中运行时,标准输入流和输出流使用的是相同的打开文件项(事实上就连标准错误流都是用的同一个文件项)。一个程序的标准输入输出使用不会产生冲突吗?查看一下当前 shell 的信息:

(py310) xxx ~ $ ls -lh /proc/self/fd/0
lrwx------ 1 xxx xxx 64 May 16 16:49 /proc/self/fd/0 -> /dev/pts/0
(py310) xxx ~ $ ls -lh /proc/$$/fd/0
lrwx------ 1 xxx xxx 64 May 16 16:43 /proc/387/fd/0 -> /dev/pts/0
(py310) xxx ~ $ cat /proc/$$/fdinfo/0
pos:    0
flags:  0100002
mnt_id: 115
ino:    3
(py310) xxx ~ $ cat /proc/$$/fdinfo/1
pos:    0
flags:  0100002
mnt_id: 115
ino:    3
(py310) xxx ~ $ file /proc/$$/fd/0
/proc/387/fd/0: symbolic link to /dev/pts/0
(py310) xxx ~ $ file $(readlink -f /proc/$$/fd/0)
/dev/pts/0: character special (136/0)

书上的例子是:

# 在 bash 中
./myscript > results.log 2>&1

这样能够将标准输出流和错误流都重定向到日志文件中去。我们希望标准输出流和标准错误流能共享文件偏移量等信息,否则两者写入同一个文件中时就会相互覆盖。

注意,这里将输出流和错误流都重定向到了一个普通文件,因而 1 和 2 号文件描述符并不是像终端那样对应着字符设备,此时文件偏移量是非常重要的信息。

#include <assert.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdio.h>

int main() {
    int fd;
    assert((fd = open("A.txt", O_WRONLY | O_TRUNC | O_CREAT,
                      S_IRUSR | S_IWUSR | S_IRGRP | S_IROTH)) != -1);
    assert(pwrite(fd, "pwrite", 6, 0) == 6);
    assert(write(fd, "WRITE", 5) == 5);
}

最后 A.txt 的文件内容为:

WRITEe

这说明第一次用 pwrite 读写完成之后,文件偏移量并没有被更新,还是在 0 的位置。