0%

管道

管道:一般指的是匿名管道

创建方式:

  1. 可以用 pipe() 创建,通过 fork() 或者 UNIX 域套接字共享给其他进程。
  2. 也可以通过 popen() 创建子进程。

popen()system() 有一些差异:

  1. system() 会为调用进程忽略 SIGINT 和 SIGTERM,但是 popen() 不会忽略这些信号,因为调用进程没有阻塞等待子进程。
  2. popen() 不会阻塞 SIGCHLD。如果阻塞了,那么在对应的 pclose() 之前就不能正常接受子进程退出的消息了。但这也有个问题:wait() 可能会接收到 popen() 创建的子进程的消息,这样调用 pclose() 的时候就会返回 -1 并设置 errno 为 ECHLD。
  3. popen()pclose() 配套。除了关闭文件描述符之外,pclose() 还会回收子进程,所以不能用 fclose() 代替 pclose()

要区分找依赖 / 共享库和找符号。

找共享库: 动态链接器在 ELF 文件中见到一个依赖名,查找此依赖的搜索顺序

在共享库中找符号:

动态加载库:dlopen API

#include <dlfcn.h>

void *dlopen(const char *filename, int flags);
int dlclose(void *handle);

使用 dlopen API 要给 gcc 传递 -ldl 参数使其链接到 libdl.so。

dlopen()

每个被加载的库会有一个引用计数,一个库被加载时,它依赖的其他库(被称为依赖树)会被自动加载,它们的引用计数增加;卸载时引用计数又会减少,归零时才真正意义上卸载共享库。

filename 参数

  • 如果 filename 包含 /,那么按照文件路径查找;否则在标准的路径中按照 41 章描述的顺序查找。
  • 如果 filename 为 NULL,那么返回主程序的句柄,也就是“全局符号对象”(global symbol object)。

在全局符号对象中符号的搜索顺序

如果用全局符号对象的句柄作为参数调用 dlsym(),那么会:

两个标准文件

utmp(/var/run/utmp)、wtmp(/var/log/wtmp)。utmp 维护当前登录进系统的用户的状态(登出后信息就被删除),后者维护每条和登录相关的信息,也就是说 who(1) 可以用前者实现,last(1) 可以用后者实现。

  1. 用户不需要知道文件的路径,使用时也应该通过 glibc 提供的宏 _PATH_UTMP_PATH_WTMP 来表示这两个文件的路径。
  2. utmp 和 wtmp 在维护用户登录之外还维护有别的信息,登录信息只是它的一部分功能而已。

Tip

lastb 命令和 last 相似,但是展示的是 /var/log/btmp 文件的内容,这里记录了那些失败的登录尝试。

getlogin() 也可以用 utmp 实现:它会从 utmp 文件中搜索当前进程的控制终端的 id(可以用 ttyname() 找到),如果找到了则返回对应记录中的登录名。

对比:getpwuid(getuid()) 是从密码文件中找到第一项 id 符合的记录,尽管用户可能使用的不是该记录关联的登录名(一个 id 可以对应多个登录名)。

操作静态库

静态库可以使用 ar 命令创建和管理(尽管一般不需要我们自己调用 ar 命令)。

创建归档:

ar r libdemo.a mod1.o mod2.o mod3.o

列出归档:

$ ar tv libdemo.a
rw-r--r-- 1000/100 1001016 Nov 15 12:26 2009 mod1.o
rw-r--r-- 1000/100 406668 Nov 15 12:21 2009 mod2.o
rw-r--r-- 1000/100  46672 Nov 15 12:21 2009 mod3.o

在概述小节的疑惑

书上说:在 Linux 上,特定的 daemon 会作为内核线程运行,比如 pdflush,用 ps(1) 列出线程时,这些 daemon 的名字会用花括号括起来。但可能因为书太老了,很多内容已经变化了。我在服务器上查出来的情况是(用我的 wsl 查不出来什么,可能内核修改太多了):

$ ps -p 1 -p 2 -o user,pgid,ppid,pid,tid,cmd
USER        PGID    PPID     PID     TID CMD
root           1       0       1       1 /sbin/init splash
root           0       0       2       2 [kthreadd]

我的理解:[kthreadd]/sbin/init 的父进程 ID 都是 0,可能表示都是被内核创建出来的?PID 和 TID 相等表示它们都是进程而不是线程。[kthreadd] 的 PGID 为 0,可能表示内核进程组,而 init 则是在一个单独的进程组中。

查找和“flush”匹配的进程:

$ ps axf -o user,pgid,ppid,pid,tid,cmd | grep flush
root           0       2       5       5  \_ [slub_flushwq]
root           0       2     549     549  \_ [kdmflush]
root           0       2     553     553  \_ [kdmflush]
root           0       2     557     557  \_ [kdmflush]
root           0       2 3639641 3639641  \_ [kworker/u65:2-flush-259:0]
amax     3642118 3618263 3642119 3642119              \_ grep --color=auto flush

本文讲的很多东西是适用于 set-user/group-ID 的。

避免使用 set-user-ID 和 set-group-ID 程序

如题。

使用尽可能少的权限

这里的权限指的是用户的身份,即 user ID。组 ID 可以类比过来,操作方式相似。

只在需要权限的时候暂时改变权限

  1. 一个 set-user-ID 的进程启动时保存其有效用户 ID。
  2. seteuid(getuid()) 更换有效用户 ID 为真实用户 ID 以执行其他程序。
  3. 在需要权限的时候暂时设置有效用户 ID,之后切换回来。

永久放弃权限

为了保证进程以后不再能获取权限,出了恢复有效用户 ID 之外,也应该恢复保存的用户 ID。但是只有进程有特权时,setuid() 才能设置真实用户 ID 和保存的用户 ID。因此,如果一个 root 用户的 set-user-ID 程序想要永久放弃权限,需要:

简介

和进程凭证相似,Linux 的能力也有区分进程和文件的,它们都有许可集、有效集和继承集(内核 2.6.24 才实现了文件能力,老内核只有进程能力)。

进程能力集合:

  • 许可集(CapPrm): TODO
  • 有效集(CapEff): TODO
  • 继承集(CapInh, inheritable): TODO

可执行文件能力集合:

  • 许可集: TODO
  • 有效集: TODO
  • 继承集: TODO

Nice 值

范围是 +19~-20,越低优先级越高,默认值为 0。它会影响时间片轮转算法下进程分到的时间长度。

  1. 资源限制参数、系统调用的返回值等场景可能会对 nice 值做一些简单的变换,而不是直接使用 +19~-20 这个范围。
  2. Nice 值可以通过系统调用设置、也有相关的命令。

实时优先级

调度策略如下:

  1. SCHED_OTHER
  2. SCHED_RR
  3. SCHED_FIFO
  4. SCHED_BATCH(Linux 2.6 增加,非标准)
  5. SCHED_IDLE(Linux 2.6 增加,非标准)

第 1 种是默认的时间片轮转策略,2、3 是实时调度策略,优先级都大于 SCHED_OTHERSCHED_BATCHSCHED_IDLE 也不是实时调度策略,调度方式和 SCHED_OTHER 类似:SCHED_BATCH 会导致频繁唤醒的任务调度次数减少,SCHED_IDLE 等价于一个非常低的 nice 值(比 +19 的优先级还要低)。

进程组

进程组和会话的作用

进程组和会话的主要作用是 shell 的作业控制。一个以 ;(或者什么都没有)结尾的命令会启用一个前台进程组,一个以 & 结尾的命令会启用一个后台进程组。在终端(窗口环境中的控制终端实际上是一个伪终端)中键入特殊字符发送信号时,信号会发给前台进程组的中的所有进程。

通常 shell 和 login(1) 会通过系统调用设置进程组和会话号。

进程组获取 / 设置的 API

可以通过 pid_t getpgrp(void) 获得当前进程的进程组 ID,通过 setpgid(pid, pgid) 来设置给定进程的进程组 ID。这两个函数的后缀不同,是因为历史原因。

对于 setpgid,如果两个参数指定了同一个进程,那么就会创建一个新的线程组;否则,指定的进程会被移动到给定的线程组中。pid 参数只能用来指定调用进程和其中一个子进程,而 pgid 参数必须是会话中的进程。另外,一个进程在子进程执行 exec() 之后就不能对其设置进程组 ID 了。

在作业控制 shell 中设置进程组

一个任务(即一个命令或一组以管道符连接的命令)中的所有进程必须被放置在一个进程组中。实际上 shell 进程需要将其他任务的进程组号设置为第一个任务的进程号。