0%

问题起因

我的某个程序会自己打印 \r\b 字符以在终端上起到提示效果,但是如果将内容重定向到文件,那么显示出来效果就不好。在很多阅读器中特殊字符不能被正确显示,在 VS Code 中 \r 会换行,而 \b 也不会真正起到删除的作用。

方法 1:cat 或者 less -r

假设现在文件 A.log 中包含了大量 \r\b,想要阅读它可以直接将其 cat 到终端,或者使用 less -r A.log 来阅读。less -r 是比较推荐的,因为还能用 / 查找、用 & 过滤

方法 2:col

如果想要真正保存一份和我们在终端看上去一样的文件,可以使用:

col -b < A.log > A1.log

实验设计

以下实验都是在 wsl 中进行的。

$ cat /proc/sys/fs/file-max   # 整个系统中能使用的最大文件描述数量
9223372036854775807
$ cat /proc/sys/fs/nr_open    # 单个进程中能使用的最大文件描述符个数
1048576

编译运行以下测试代码(不要用 Release 模式、不要增加 -DNDEBUG;这份代码还假设了 0、1、2 号文件描述符已经被打开):

#include <cassert>
#include <cerrno>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <sys/resource.h>
#include <unordered_set>
#include <vector>

int main() {
  // check rlimit
  int ret;
  struct rlimit rlimit_res;
  if ((ret = getrlimit(RLIMIT_NOFILE, &rlimit_res)) == -1) {
    perror("getrlimit");
    exit(EXIT_FAILURE);
  }
  printf("soft limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_cur);
  printf("hard limit of fd count: %lu\n", (unsigned long)rlimit_res.rlim_max);

  // try to open as many files as we can
  std::vector<int> opened_files{0, 1, 2};
  while (true) {
    int fd = open("/dev/zero", O_RDONLY);
    if (fd == -1) {
      if (errno == EMFILE) {
        break;
      } else {
        perror("open");
        exit(EXIT_FAILURE);
      }
    }
    opened_files.push_back(fd);
  }
  // check if sorted
  for (size_t i = 1; i < opened_files.size(); ++i) {
    assert(opened_files[i - 1] < opened_files[i]);
  }
  // find missing fds
  {
    int maxfd = opened_files.back();
    std::unordered_set<int> st(opened_files.begin(), opened_files.end());
    for (int i = 0; i < maxfd; ++i) {
      if (!st.count(i)) {
        printf("missing fd %d\n", i);
      }
    }
  }
  printf("fd count: %zd\n", opened_files.size());
}

实验结果

直接通过 shell 进入 WSL:Debian 时,运行结果如下:

想要搜索 bash 内置命令的帮助信息,但是发现找不到,比如 man ulimit 没有对应的页面,怎么办呢?

其实和 bash 内置命令相关的帮助信息就在 bash 的 man 手册当中,可以用 man bash 来查看。然后在弹出的 less 阅读器中,搜索 SHELL BUILTIN COMMANDS 就能找到这一栏了(用小写就能搜索)。

整体概览

终端可以分成两种工作模式:1. 规范模式(输入按照行来处理);2. 非规范模式,比如 vim、less 等程序中。

终端驱动程序的作用是:操作两个队列,一个用于从终端设备把输入字符传送到读取进程上,另外一个用于将输出字符从进程传输到终端上。如果开启了终端回显功能,那么终端输入队列上面的新字符也会自动被追加到输出队列的尾部。终端驱动程序还能识别终端输入中的特殊字符,并根据字符含义做出相应的行为(比如 ctrl + d 和 ctrl + c)。

如果进程想要知道终端上面有多少没有读取的内容(假设终端的是其标准输入),在 Linux 上面可以使用 ioctl(fd, FIONREAD, &cnt)。这个特性在 SUS 中没有规定。

获取和修改终端属性

可以用 tcgetattr(2) 和 tcsetattr(2) 操作终端属性。这只能由前台进程调用,如果是后台进程,会因为没有控制终端而收到终端驱动程序发来的 SIGTTOU 信号。

概览

  • I/O 多路复用,select()poll(),检查大量文件描述符时性能不好
  • 信号驱动 I/O
  • Linux 特有的 epoll()
  • POSIX 异步 I/O(AIO),在本书不讲(应该也不是一种通知模型)

Tip

Libevent 库为包括 select()poll()epoll()、信号驱动 I/O、Solaris 专有的 /dev/poll 和 BSD 专有的 kqueue 接口在内的很多 I/O 方式提供了抽象层。

通知文件描述符就绪的两种方式是水平触发和边缘触发。

  • 水平触发:文件描述符上可以非阻塞执行 I/O 调用,则认为已经就绪。select() / poll()epoll() 可以支持水平触发模型。
  • 边缘触发:文件描述符自上次检查以来有了新的 I/O 活动,则认为需要通知。信号驱动 I/O 和 epoll() 可以支持水平触发模型。

书上还提到边缘触发时一般需要尽可能读取完所有字节,以免很长时间没有接收到下一次通知。比如:在循环中每次检查是否有数据可以读,如果有,则最多读取 1024 个字节,然后进入下一个循环,这样的操作就只能在水平触发模型上正常工作。这是因为只要状态没有改变,边缘触发模式下的通知系统就不会再次发送通知,如果只最多读取 1024 个字节,后面即便有数据没有读取完成,也不会有新的通知了。

我的测试环境是 wsl,系统是空载的。理想的情况是两次程序的 pid 连续:

$ grep -i '^pid:' /proc/self/status
Pid:    3355
$ grep -i '^pid:' /proc/self/status
Pid:    3356

但实际上我发现我每次回车(不运行 grep)都会导致 Pid +7,运行 grep 时会 +8(合理,毕竟 grep 本身也是一个进程)。

我以前以为是 bash 在创建子进程的时候会多运行一些东西,所以每次创建的子进程 Pid 不连续。现在发现回车都会导致 Pid 增长,才发现是 PS1 变量的问题。我的 PS1 变量包含了一些代码,用来让 prompt 更加美观。

用以下方式修改 PS1

部分读和部分写

套接字上面可能发生部分读和部分写,书中提供了 writen() 和 readn() 函数来保证读写完数据,接口和 write() / read() 一样,可以借鉴一下这种思路。

shutdown() 函数

可以指定关闭 socket 的读 / 写 / 读写。如果不用 shutdown(),打开的 socket(文件描述)只会在所有指向它的文件描述符都关闭了之后才会被关闭。

  • SHUT_RD:关闭读端。在 UNIX 域流套接字上面执行了这个操作之后,对端应用程序写入会受到 SIGPIPE 信号(或者 EPIPE 错误,如果屏蔽或处理了这个信号)。对于 TCP 套接字,关闭读端没有意义。
  • SHUT_WR:关闭写端。读端能继续读,读完之后能看到 EOF,写端不能再写(SIGPIPE 和 EPIPE)。
  • SHUT_RDWR:相当于先执行 SHUT_RD 再执行 SHUT_WR

recv()send()

接口相比 read()write() 多了一个 flags 参数,能在 socket 操作上面对本次读写实现更多的功能控制。

sendfile()

sendfile() 能将文件(fd)的指定范围(offset 和 count)的数据直接发送到套接字中。与先读数据后写入相比,这样的操作不需要经过用户缓冲区,能直接在内核缓冲区完成传输。

下载前使用 -F 选项来查看视频格式:

yt-dlp -F https://www.youtube.com/watch?v=lNPZV9Iqo3U

下载选定格式的视频(yt-dlp 给 YouTube 视频选的默认格式通常很模糊,但是因为同时包含音频和视频,所以就成了默认):

yt-dlp -N 4 -f 137+139 https://www.youtube.com/watch?v=lNPZV9Iqo3U

-N 4 表示分成 4 块下载,默认是 1 块。-f 表示格式,如果安装了 ffmpeg,可以选择多个格式,这些格式会被合并到一个视频中(audio only 和 video only 的格式的合并)。

std::shared_ptr<T> 的内存开销

std::shared_ptr<T>
    element_type*    _M_ptr;          // Contained pointer. sizeof(intptr_t) 字节
    __shared_count<_Lp>  _M_refcount; // Reference counter. sizeof(intptr_t) 字节
        _Sp_counted_base<_Lp>*  _M_pi;
std::_Sp_counted_base<__default_lock_policy>
    // vtable pointer               // sizeof(intptr_t) 字节
    _Atomic_word  _M_use_count;     // #shared                4 字节,实际上是 int 类型
    _Atomic_word  _M_weak_count;    // #weak + (#shared != 0) 4 字节

其中,记录 use count 是为了判断什么时候可以释放共享指针指向的对象;记录 weak count 是为了判断什么时候可以安全释放控制块本身。即便是共享指针指向对象已经被释放(use count 归零),也可能有弱指针会尝试转换成共享指针,因此应该保证这些弱指针能安全查询控制块。还有一点,如果用 std::make_shared 创建共享指针,use count 归零而 weak count 不归零时,共享对象只是被析构,其内存会等到 weak count 归零时一起被释放。可以参考 https://stackoverflow.com/a/49585948/

在 64 位环境下,一个 std::shared_ptr<T> 是 16 字节(8 + 8),其中控制块的真实大小是 16 字节(4 + 4 + 8)。(在 std::shared_ptr<T> 中存储的是指向控制块的指针、而不是控制块本身,这样才能保证使用相同的控制块。)

std::_Sp_counted_base<_Lp> 的大小

不过,std::_Sp_counted_base 还支持其他的上锁策略,从 /usr/include/c++/12/bits/shared_ptr_base.h 的以下代码可以看出一共有 4 种可选的上锁类型。

说明

因为项目有老代码所以才需要这样处理,一般建议用 RAII 等技术避免裸露资源。

find_malloc.sh

要点:

  1. 用 gcc 去除代码注释。
  2. 用 awk 对正则表达式计数。
check() {
  gcc -fpreprocessed -dD -E -P "$1" 2>/dev/null | awk -v file="$1" '
    BEGIN {
      malloc=0;
      free=0;
      queueCreate=0;
      queueDestroy=0;
      notifierCreate=0;
      notifierDestroy=0;
      handleCreate=0;
      handleDestroy=0;
    }
    /cnrtMalloc\(/          { malloc++; }
    /cnrtFree\(/            { free++; }
    /cnrtQueueCreate\(/     { queueCreate++; }
    /cnrtCreateQueue\(/     { queueCreate++; }
    /cnrtDestroyQueue\(/    { queueDestroy++; }
    /cnrtQueueDestroy\(/    { queueDestroy++; }
    /cnrtCreateNotifier\(/  { notifierCreate++; }
    /cnrtDestroyNotifier\(/ { notifierDestroy++; }
    /cnnlCreate\(/          { handleCreate++; }
    /cnnlDestroy\(/         { handleDestroy++; }
    END {
      if (malloc != free) {
        print "file: " file ", malloc: " malloc ", free: " free
      }
      if (queueCreate != queueDestroy) {
        print "file: " file ", queueCreate: " queueCreate ", queueDestroy: " queueDestroy
      }
      if (notifierCreate != notifierDestroy) {
        print "file: " file ", notifierCreate: " notifierCreate ", notifierDestroy: " notifierDestroy
      }
      if (handleCreate != handleDestroy) {
        print "file: " file ", handleCreate: " handleCreate ", handleDestroy: " handleDestroy
      }
    }'
}

check $1

find_malloc_all.sh

要点:使用 find 匹配时应该选择正则表达式类型,同时和 Python 的 re 模块一样要全字符串匹配(不能匹配只部分字符,因此想只匹配中间部分的时候,就要在两边加上 .*)。