0%

迭代型服务器和并发型服务器

迭代型服务器每次只处理一个客户端,在处理客户端请求时无法响应其他客户端;并发型服务器每次接受新的请求后,就会创建新的线程或者进程去专门处理这个请求。

inetd(Internet 超级服务器)守护进程

/etc/services 下列举着大量服务,但是每个服务器只是偶尔运行。inetd 进程通过监听一组 socket 端口并按需启动对应的服务来节省系统开销。因此 inetd 又被叫做 Internet 超级服务器。守护进程 inetd 的行为由配置文件 /etc/inetd.conf 控制(在我的 WSL(Debian)上面没有安装 inetutils-inetd,所以没有这个配置文件)。修改配置文件之后,向 inetd 发送 SIGHUP 可以要求其重载配置。

许多发行版有更加先进的 xinetd(8)。

几列数据分别是服务的名称(可以根据 /etc/services 查到服务对应的端口,理论上有些服务在不同协议下有可能使用不同端口,但是我看 /etc/services 中的服务都是 udp 和 tcp 用相同的端口号)、socket 的类型、协议、标记(要么是 wait,要么是 nowait;wait 表示在子进程运行时暂时将对应 socket 从监听列表中移除,等子进程退出后再次加入监听)、登录名(控制子进程的用户名和组名,由于 inetd 是 root 运行的,所以不控制子进程的凭据会有很大的风险)、程序位置、参数列表(图中只提供了 aegv[0])。

OneDrive 可以同步桌面,我以前的桌面是 C:\Users\xxx\OneDrive\Desktop,现在我的新电脑的桌面是 C:\Users\xxx\Desktop,虽然 OneDrive 仍然保存了之前的文件夹,但是没有对桌面进行同步。

解决方案是从文件资源管理器进入用户目录,然后右键桌面,选择位置 > 移动。点击移动之后桌面上面的文件就会被移动到新的路径(新路径的旧文件不会被删除,遇到同名文件会询问是否覆盖),随后新的路径会被设置为桌面。

多宿主机(Multihomed host)指拥有多个网络接口的主机(可以是个人计算机,也可以是路由器)。

在网络层中,IPv4 为 IP 头提供了校验和,这样就能检测出头中的错误,但是 IPv4 并没有为包的内容提供校验。IPv6 则没有为 IP 头提供校验和。在传输层中,UDP 校验和在 IPv4 上可选、在 IPv6 上强制;TCP 校验和总是强制的。

常量 INADDR_ANY 就是所谓的 IPv4 通配地址,大多数实现将其定义成了 0.0.0.0。使用通配地址,多宿主机可以接收任意一个主机 IP 地址的数据。

IPv6 的环回地址为 ::1,通配地址为 0::0 或者 ::(同一个值的两种写法而已)。IPv6 还提供了“IPv4 映射的 IPv6 地址”,这样的 IPv6 地址是在 IPv4 地址前面加上 ffff(两个字节的全 1),然后再加上一堆 0 形成的。与 204.152.189.116 等 价 的 IPv4 映 射 的 IPv6 地 址 是 ::FFFF:204.152.189.116。

Socket 基础

Socket 分为 UNIX Domain Socket 和 Internet Domain Socket,两者都有数据报和字节流两种工作模式。前者(UNIX Domain Socket)是同一台主机不同进程之间通信的方式,是可靠的通信(包括数据报通信方式)。

SUS 规定 socket 至少可选以下的 domain,具体实现可以提供更多选择:(AF 表示地址族,PF 表示协议族,早期设计人员认为一个协议族可以支持多个地址族,但实际上协议族和地址族基本上是一一对应的关系,而且所有 PF_ 开头的常量都被定义成对应的 AF_ 开头的常量。)

SUS 规定 socket 至少可选流(SOCK_STREAM)和数据报(SOCK_DGRAM),具体实现可以提供更多选择,比如 SOCK_RAW 表示直接使用 IP 而不是传输层协议,但这可能需要特权。在流类型的 socket 中,连接的另外一方被称为 peer socket(对等 socket)。

流 socket

UNIX 域套接字 bind() 时会在文件系统上面创建文件

UNIX domain socket 在 bind 的时候会在文件系统上面创建一个 socket 文件。

如果文件系统上面已经有该路径,则会绑定失败。往往是服务器在 bind() 之前调用 remove() 来尝试删除旧文件(remove() 既可以删除文件夹又可以删除普通文件),在不再需要 socket 时也会立即调用 unlink() 来删除 socket,等所有使用该 socket 的进程退出之后文件就会被文件系统自动清理。

Linux 特有的抽象 socket 名字空间

bind() 的时候把 struct sockaddr_unsun_path 字段的第一个字节指定为 '\0',后面仍然填 socket 名字,则可以为 socket 创建一个抽象名字,这样 socket 就不会出现在文件系统上面,也不会和文件系统上面的条目冲突。

UNIX domain 数据报 socket 能传输的最大大小

通过 SO_SNDBUF 选项和 /proc 文件系统下面的限制表示。可以参考 socket(7)。我本地看是 212992。

为 UNIX domain socket 创建 socket 对

socketpair() 系统调用有一点像 pipe() 系统调用,前者会创建匿名的 socket 对,可以跨过 fork() 共享给子进程。这个系统调用为我们省去了 socket()bind()listen()connect()accept() 等环节,而且创建的 socket 对外是不可见的。

Tip

本文没有得到最终结论,只是一些个人猜想。

CUDA Kernel 常用 float 类型这件事 这篇笔记提到一个 issue 里面说 ATen 使用 double 作为 32 位浮点数的累加类型是因为用 float 会挂掉一个 batchnorm 的测试。这在我看来不可思议,因为 GPU 上面测试都没有挂,为什么 CPU 上面反而会挂呢?本文记录笔者看 batchnorm 的实现、试图找到原因的过程。

PyTorch 的 batchnorm 要对每一轮的输入计算均值和标准差,其中计算均值就需要将输入都加起来(再除以元素总数),这中间就可能产生累加误差。标准差的计算也类似,会有累加产生的误差。

从 aten/src/ATen/native/native_functions.yaml 中可以找到 native_batch_norm() 函数是在哪里实现的:

- func: native_batch_norm(Tensor input, Tensor? weight, Tensor? bias, Tensor? running_mean, Tensor? running_var, bool training, float momentum, float eps) -> (Tensor, Tensor, Tensor)
  dispatch:
    CPU: batch_norm_cpu
    CUDA: batch_norm_cuda
    MPS: batch_norm_mps
    MkldnnCPU: mkldnn_batch_norm

本文分别讨论双精度、单精度、半精度的浮点数计算,最后提及混合精度。在 CPU 方面,仅考虑 x86-64 CPU 和 GNU/Linux 上的 GCC 编译器;GPU 方面仅考虑 NVIDIA GPU。

GPU 上双精度计算慢在哪里?

https://docs.nvidia.com/cuda/cuda-c-programming-guide/index.html#arithmetic-instructions 以上链接说明:GPU 双精度浮点数运算比单精度浮点数慢,在有些架构(很多 $x.y~(y \ne 0)$ 运算能力的 GPU 都是游戏卡)上甚至慢得多。除了指令慢之外,double 类型也不利于 cache 和全局内存带宽。

https://forums.developer.nvidia.com/t/use-float-rather-than-double-in-a-kernel/107363 A 64 bit double variable takes 2 registers. A 32 bit float can be stored in 1 register. 双精度浮点数的使用会增加单个 CUDA 线程对寄存器数量的需求,从而减少实际上同时可以运行的线程数。

怎么正确使用单精度类型?——谨防隐式转换成双精度

CUDA 和 C/C++ 都不会先将 float 转 double 再计算,除非……

https://godbolt.org/z/Me66bEeaa 我在本地尝试构造浮点数精度损失,但是无法构造出来,去 compiler explorer 一看发现实际上单精度浮点数被转换成了双精度浮点数计算。为什么呢?

https://godbolt.org/z/n5bjMGcx3

编译器并不会自动根据代码的依赖关系去编排静态初始化顺序,示例代码中 vec 在被推入两个元素之后又被初始化了一次(在 compiler explorer 中看汇编也能看出来)。

#include <iostream>
#include <vector>

extern std::vector<int> vec;

void report_size() {
    printf("vec.size(): %zd\n", vec.size());
}

auto _1 = []() {
    vec.push_back(0);
    printf("first  ");
    report_size();
    return 0;
}();

auto _2 = []() {
    vec.push_back(0);
    printf("second ");
    report_size();
    return 0;
}();

std::vector<int> vec;

int main() {
    printf("main   ");
    report_size();
}

输出:

first  vec.size(): 1
second vec.size(): 2
main   vec.size(): 0

大概特点

POSIX 信号量有两种:命名信号量和匿名信号量,前者和 System V 信号量比较相似(System V 的 IPC 都用 key 来标识,因此相当于是命名的)。

POSIX 信号量使用了 futex(2) 来实现,在没有争抢的情况下,不会发生系统调用,因此效率比 System V 实现更高。在争抢频繁的情况下,两者性能差不多。

和 POSIX 消息队列类似,Linux 上的 POSIX 信号量被挂载在 /dev/shm 目录这个 tmpfs 文件系统下。该文件系统具有内和持久性。

(二元)信号量和 pthreads mutex 相比:

  1. 前者是异步信号安全的,后者不是。但是处理信号还是建议用 sigwaitinfo()
  2. 前者可以由任何线程释放资源(sem_post()),后者只能由锁的持有者释放,否则是未定义行为。

POSIX 共享内存对应虚拟文件系统 /dev/shm,这是一个 tmpfs 文件系统,具有内核持久性。如果不满意默认的大小(书上说默认大小是 256M,但是我测试默认大小是内存的一半——在服务器、虚拟机、wsl 上都是这样;docker 容器共享内存的默认大小则是 64M),可以使用 mount -o remount,size=<num_bytes> ... 重新挂载。

回忆:PyTorch 的 dataloader 经常需要共享内存,因此创建的跑 PyTorch 程序的容器需要设置更大一点的共享内存限制。

POSIX 共享内存的使用方式很像一个文件,可以用 shm_open() 打开或者创建,用 shm_unlink() 来删除。

SYNOPSIS
       #include <sys/mman.h>
       #include <sys/stat.h>        /* For mode constants */
       #include <fcntl.h>           /* For O_* constants */

       int shm_open(const char *name, int oflag, mode_t mode);
       int shm_unlink(const char *name);

POSIX 共享内存对象要和 mmap() 一起用:先打开一个共享内存对象,然后将其文件描述符映射到内存的某处。这个时候 shm_open()mmap() 的关系很像 shmget()shmat() 的关系。