0%

有的时候我们希望私有化构造函数,然后要求用户只通过工厂方法访问我们的类型,在 std::enable_shared_from_this 的例子 中就有使用。这个例子是从 cppreference 上面抄来的。但是今天我发现去年 11 月有人修改了网页上的例子,修订记录为 https://en.cppreference.com/mwiki/index.php?title=cpp%2Fmemory%2Fenable_shared_from_this&diff=162885&oldid=153414

后来这个例子又有了新的修订,现在这个例子是:

class Best : public std::enable_shared_from_this<Best>
{
    struct Private{ explicit Private() = default; }; // 这个 explicit 构造函数非常重要
 
public:
    // Constructor is only usable by this class
    Best(Private) {}
 
    // Everyone else has to use this factory function
    // Hence all Best objects will be contained in shared_ptr
    static std::shared_ptr<Best> create()
    {
        return std::make_shared<Best>(Private());
    }
 
    std::shared_ptr<Best> getptr()
    {
        return shared_from_this();
    }
};

构造函数不再私有,调用者能明确看到需要一个私有的标记类,从而对 API 的使用方式会更加清楚。

尤其是 Private 类中的 explicit Private() = default; 非常重要,如果没有这个,使用者可以通过避免明确书写 Private 类型而构造出 Private 类型的对象!这是非常危险的。

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

找共享库: 动态链接器在 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 进程需要将其他任务的进程组号设置为第一个任务的进程号。