Linux/Unix系统编程手册 整理笔记

只列重点和延伸

基本概念

  • 文件I/O模型
  • 进程
    • 内存布局,文本,数据,堆,栈
    • _exit 退出 (实现是调用sys_exit退出,c标准库中的exit比这个函数多了一些清空IO 的动作,遇到过一次和log4cplus挂死的问题)
      • 后面的章节会讲这个
    • 进程的用户和组标识符,凭证,限定权限
    • 特权进程,用户ID为0 的进程,内核权限限制对这种进程无效
    • 不同用户的权限有不同的能力,可以赋予进程来执行或者取消特殊的能力 CAP_KILL
    • init进程 所有进程之父 创建并监控其他进程
    • 守护进程
    • 环境列表 export setenv char **environ
    • 资源限制 setrlimit()
  • 内存映射 mmap
    • 共享映射
      • 同一文件
      • fork继承
        • 有标记来确认映射改动是否可见
  • 进程间通信以及同步
    • 信号 ctrl c
    • 管道 , FIFO, pipe
    • socket
    • 文件锁定 (典型使用举例?)
    • 消息队列 (典型使用举例?)
    • 信号量
    • 共享内存
  • 进程组,信号传递 (典型使用举例?)(ctrl c?)
  • 会话,控制终端和控制进程 ctrl c
  • 伪终端,ssh
  • 日期和时间 真实事件和进程时间 time(time有两个,另一个是看更细致的内核动作的)
  • /proc文件系统

系统编程概念

  • 系统调用
    • 调用c wrapper函数,入参复制到寄存器,触发中断,切换到内核态,通过中断向量表找到系统调用
      • 中断改成sysenter了
    • 内核栈保存寄存器的值,校验系统编号,正式调用sysytemcall
      • 可以用strace抓
  • 库函数
    • 版本号
    • errno perror 封装一些错误处理和解析函数
  • 可移植性问题,指的是一些宏开关,BSD POSIX GNU_SOURCE之类的

文件IO: 通用的IO模型

  • open
    • flag 只列有意思的,后面还会讲
      • O_CLOEXEC fcntl
      • O_NONBLOCK 非阻塞io
      • O_ASYNC 信号驱动io, fcntl(file control)
    • err
      • 无法访问,目录问题,文件描述符上限,文件打开数目上限,文件只读,文件为exe
  • read 返回值,错误-1没了0读到多少返回多少
    • 内核维护读到那里,aka偏移量 lseek
  • write 返回值,写入了多少。可能和指定的count不一致(磁盘满/RLIMIT_FSIZE)
    • 偏移量超过文件结尾继续写入 aka文件空洞 eg: coredump
      • 文件空洞占不占用?严格说占用,看空洞边界落在哪里,落在文件块内还是会分配空间的 用0填充
  • ioctl (io control)

  • 原子操作和竞争条件
  • fcntl更改文件状态位
  • 文件描述符与打开文件的关系
    • 内核维护的数据结构
      • 进程及文件描述符表
        • fd
        • flag
      • 系统级打开文件表
        • 文件句柄
        • 文件偏移量,状态,访问模式,inode引用
      • 文件系统inode表
        • 文件类型,访问权限,锁指针,文件属性
      • 多个文件描述符可以对应同一个句柄,共用同一个偏移量
        • 复制文件描述符 dup/dup2 2>&1
      • O_CLOSEXEC 描述符私有
  • 特定偏移 pread pwrite
  • 分散输入和集中输出scatter-gather readv writev
  • 阶段文件truncate ftruncate
  • 大文件IO
  • /dev/fd ?

进程

  • pstree
  • 内存布局
  • 虚拟内存管理
    • 空间局部性和时间局部性
    • 驻留集 resident set和交换区swap area
    • 有效虚拟地址范围发生变化
      • 栈增长到之前没到过的地方
      • malloc brk sbrk提升program break位置
      • 共享内存访问 shmat shmdt
      • mmaSp/munmap
    • 地址空间隔离的有点
      • 进程进程 进程内核间隔离
      • 共享内存
        • 同一份代码副本
        • shmget mmap共享内存
      • 内存保护
      • 每个进程使用的真实内存少,使得容纳的进程多,CPU利用率高
    • 栈和栈帧 不多说
    • 环境 env 不多说
    • setjmp longjmp 别用

内存分配

  • 堆当前边界 program break
    • 调整边界brk/sbrk
    • malloc free实现,老生常谈了
      • 调试malloc
        • mtrace muntrace 搭配MALLOC_TRACE mtrace 分析文件写入
        • mcheck mprobe 一致性检查分析
        • MALLOC_CHECK_ 环境变量,提供上面的功能,动态的,安全原因,设置用户id组id的程序无法设置
        • glibc检测 mallopt mallinfo
    • calloc realloc
    • 对齐分配 memalign posix_memalign
    • 栈上分配alloca
      • 邪门歪道别乱用

页帧分配(Page frame allocation)

页是物理内存或虚拟内存中一组连续的线性地址,Linux内核以页为单位处理内存,页的大小通常是4KB。当一个进程请求一定量的页面时,如果有可用的页面,内核会直接把这些页面分配给这个进程,否则,内核会从其它进程或者页缓存中拿来一部分给这个进程用。内核知道有多少页可用,也知道它们的位置。

伙伴系统(Buddy system)

Linux内核使用名为伙伴系统(Buddy system)的机制维护空闲页,伙伴系统维护空闲页面,并且尝试给发来页面申请的进程分配页面,它还努力保持内存区域是连续的。如果不考虑到零散的小页面可能会导致内存碎片,而且在要分配一个连续的大内存页时将变得很困难,这就可能导致内存使用效率降低和性能下降。下图说明了伙伴系统如何分配内存页。 buddy-system

如果尝试分配内存页失败,就启动回收机制。可以在/proc/buddyinfo文件看到伙伴系统的信息。

页帧回收

如果在进程请求指定数量的内存页时没有可用的内存页,内核就会尝试释放特定的内存页(以前使用过,现在没有使用,并且基于某些原则仍然被标记为活动状态)给新的请求使用。这个过程叫做内存回收kswapd内核线程和try_to_free_page()内核函数负责页面回收。

kswapd通常在task interruptible状态下休眠,当一个区域中的空闲页低于阈值的时候,它就会被伙伴系统唤醒。它基于最近最少使用原则(LRU,Least Recently Used)在活动页中寻找可回收的页面。最近最少使用的页面被首先释放。它使用活动列表和非活动列表来维护候选页面。kswapd扫描活动列表,检查页面的近期使用情况,近期没有使用的页面被放入非活动列表中。使用vmstat -a命令可以查看有分别有多少内存被认为是活动和非活动状态。

kswapd还要遵循另外一个原则。页面主要有两种用途:页面缓存(page cahe)进程地址空间(process address space)。页面缓存是指映射到磁盘文件的页面;进程地址空间的页面(又叫做匿名内存,因为不是任何文件的映射,也没有名字)使用来做堆栈使用的,在回收内存时,kswapd更偏向于回收页面缓存。

Page out和swap out:“page out”和“swap out”很容易混淆。“page out”意思是把一些页面(整个地址空间的一部分)交换到swap;”swap out”意味着把所有的地址空间交换到swap。

如果大部分的页面缓存和进程地址空间来自于内存回收,在某些情况下,可能会影响性能。我们可以通过/proc/sys/vm/swappiness文件来控制这个行为

swap

在发生页面回收时,属于进程地址空间的处于非活动列表的候选页面会发生page out。拥有交换空间本身是很正常的事情。在其它操作系统中,swap无非是保证操作系统可以分配超出物理内存大小的空间,但是Linux使用swap的空间的办法更加高效。如图1-12所示,虚拟内存由物理内存和磁盘子系统或者swap分区组成。在Linux中,如果虚拟内存管理器意识到内存页已经分配了,但是已经很久没有使用,它就把内存页移动到swap空间。

像getty这类守护进程随着开机启动,可是却很少使用到,此时,让它腾出宝贵的物理内存,把内存页移动到swap似乎是很有益的,Linux正是这么做的。所以,即使swap空间使用率到了50%也没必要惊慌。因为swap空间不是用来说明内存出现瓶颈,而是体现了Linux的高效性。

ps -o majflt,minflt -p pid

minor fault 在内核中,缺页中断导致的异常叫做page fault。其中,因为filemap映射导致的缺页,或者swap导致的缺页,叫做major fault;匿名映射导致的page fault叫做minor fault。 作者一般这么区分:需要IO加载的是major fault;minor fault则不需要IO加载


用户和组

  • 密码与密码文件/etc/shadow

  • /etc/group

    跳过了,没啥说的。


进程凭证

讲了一大堆关于用户组,权限之类的东西


时间

  • gettimeofday
    • time 多余的
    • ctime打印用
  • 时区TZ
  • 地区LC_ALL
  • 软件时钟jiffies
  • 进程时间
    • time命令,有两个

系统限制和选项

  • sysconf getconf pathconf

系统和进程信息

  • /proc

    • cat /proc/pid/status

    • 关注env status cwd fd maps mem mounts task

      image-20200620203233057

  • uname


文件IO缓冲

  • 用户空间缓冲区和内核空间缓冲区之间的数据复制,不会直接发起磁盘访问

  • 系统调用越少越好

  • 刷新stdio fflush

  • fsync 刷盘,包括元数据更新 fdatafync可能会减少磁盘操作的次数

    • 写入同步O_SYNC

    image-20200623144236672

  • 绕过缓存 直接IO O_DIRECT

    • 必须对齐

系统编程概念

  • 设备文件 /dev
    • 字符型设备 终端键盘
    • 块设备,磁盘,512倍数
  • 文件系统
    • 引导块,超级块 i节点表 数据块
    • inode 文件元数据
      • 数据块指针
        • lseek,算指针就行了
  • VFS
  • 日志文件系统
  • 挂载mount 太复杂了。我还是现搜现用吧
  • tmpfs

文件属性 没啥说的

  • stat
  • chown
  • utime

  • 扩展属性setattr

目录与链接

  • 软链接硬链接 没啥说的,inode
  • unlink
  • raname
  • nftw遍历目录树? 没发现啥使用场景

监控文件事件

inotify可以和epoll串起来

和内核交互,会耗费内核内存,所以有限制

​ /proc/sys/fs/inotify


信号

signal handler

kill 发送信号

  • raise相当于自己调用kill(getpid(),sig)
  • killpg

信号掩码,没用过

可重入要考虑

终止signal handler

  • _exit, exit不安全,这是个典型问题了 exit会刷stdio,可能会卡死
  • kill
  • abort
  • setjmp longjmp 有点魔法,没见过谁用

系统调用期间遇到的信号 -EINTR, 可能系统调用体检结束失败了。

利用这个特性,可以为阻塞调用设置一个定时器

也可以手动重启调用

  • 对应有个NO_EINTR的宏
#define NO_EINTR(stmt) while((stmt) == -1 && errno = EINTR)

​ 这样循环执行忽略EINTR错误,比较不方便,但我感觉直接屏蔽信号更好一些

  • 用sigaction指定SA_RESTART让内核帮助重试调用,但是这只是部分有效,比如poll这些肯定是无效的

高级特性

  • core文件
    • /proc/sys/kernel/core_pattern
  • TASK_INTERRUPTIBLE -> S , TASK_UNINTERRUPTIBLE -> D
  • 如果用了信号阻塞,信号恢复后怎么传递?序号升序 ,越小优先级越高
  • signal函数用sigaction实现的。

定时器和休眠

settitimer alarm

alarm会让阻塞的系统调用产生EINTR,也就是超时结束了

但这东西可能有竞态问题,还是用select /poll超时特性更好,还能整合到轮训框架内

nanosleep


进程

创建

  • 父子进程文件共享
    • fd操作文件表项,包含当前文件原信息,修改会互相影响
  • 父子进程谁先谁后?
    • 2.6.32之后默认父在前,有点点性能优势,但差异很小
    • 同步信号规避竞争

终止

  • _exit和exit说了好几遍了
    • exit会主动调用atexit/on_exit注册函数,清理stdio,然后在调用_exit所以期间可能会锁死
    • main函数return n等于exit(n)
  • 更多终止细节
    • 关闭打开的文件描述符 目录流各种描述符
    • 释放各种文件锁
    • 分离各种共享内存段
    • 如果不是daemon,会向终端发送SIGHUP
    • system v信号量 semadj +到信号量值里(没懂这块)
    • 关闭各种posix有名信号量sem_close
    • 关闭各种posix消息队列 mq_close
    • 进程组孤儿 SIGHUP + SIGCONT?
    • 溢出各种内存所mlock
    • 取消各种内存映射mmap

fork和stdio缓冲区的问题

书中的例子printf是有’\n’的

输出到终端是行缓冲会直接刷新到终端,输出到文件是块缓冲,不会立即刷到文件,这就复制了两份缓冲区

如果printf没有‘\n’还是会有缓冲区的问题

标准输出是行缓冲,所以遇到“\n”的时候会刷出缓冲区,但对于磁盘这个块设备来说,“\n”并不会引起缓冲区刷出的动作,那是全缓冲

看参考链接 1 2能加深理解

解决方法,

  • 手动flush
  • 手动设置缓冲为0 setvbuf
  • 子进程_exit跳过刷新缓冲区

孤儿进程和僵尸进程

  • 需要父进程wait,不然占用内核空间
  • 主动处理SIGCHLD或者子进程忽略SIGCHLD 直接系统丢弃子进程状态

文件描述符与exec

FD_CLOSEXEC

为什么system实现要阻塞SIGCHLD,忽略SIGINT和SIGQUIT信号?


线程

  • errno一个线程一个

  • pthread api返回值
  • 链接带上lpthread
  • 可重入函数要注意

创建线程

  • pthread_create
    • pthread_self获取

终止线程

  • 线程函数return/exit
  • pthread_exit
    • pthread_join获取返回值
  • pthread_cancel

  • thread_join 类似waitpid
    • 特定的tid(waitpid可以任意pid)
    • 阻塞的(可以自己用condvar搞非阻塞)
    • 对等的(waitpid只能是父等子)
  • pthread_detach
  • pthread_attr_init 属性设置

更多细节

  • 线程栈
  • 线程和信号

线程同步

互斥量mutex -> futex虽然慢也比fcntl信号量这种系统调用要快

注意死锁

condvar 经典问题,为啥用while守着signal


线程安全

可重入

一次性初始化 pthread_once std::call_once

一个线程安全的singleton是什么样的


进程组 会话和作业控制

find / 2> /dev/null | wc -l &

find命令和wc命令同进程组

find命令和wc命令和当前bash同会话,bash是会话首进程

一个终端也就只有一个会话

前台进程和后台进程组,前台进程就一个or没有

进程控制 nohup SIGHUP

作业控制 jobs


进程优先级和调度

nice值和分配策略

cpu亲和

使用的资源与资源限制

防范

  • 特权?

  • chroot jail?
  • 缓冲区溢出
  • 不可信用户输入
  • DDoS

能力

  • 用户ID ?root?

daemon

永远不会成为会话组长,也就永远不会成为控制终端

关掉不用的0 1 2,不要浪费文件描述符


共享库

使用共享库的有用工具

  • ldd
  • readelf/objdump
  • nm

版本与命名规则


进程间通信

通信工具

  • 数据传输工具
    • 字节流 :文件是一个字节序列 读取动作是消耗性质的
      • 管道
      • FIFO
      • 数据报socket
    • 消息队列
      • system V消息队列
      • posix 消息队列
      • 数据报socket交换
    • 伪终端
  • 共享内存 速度快但是需要同步 所有进程都可见
    • system V共享内存
    • posix共享内存
    • 内存映射

同步工具

  • 信号量
  • 文件锁
  • mutex condvar
  • 通信工具也可以用来同步,pipe eventfd

管道pipe 和FIFO

管道

  • 字节流,单向
  • 容量有限,写入阻塞
  • 进程同步方法
  • 管道和缓冲
    • 伪终端替换管道

FIFO aka命名管道

屏蔽SIGPIPE

阻塞IO 非阻塞IO设置?


system v ipc

消息队列/信号量/共享内存

ipc key:整数 IPR_PRIVATE/ftok

ipcs ipcrm 类似ls rm

ipcs

------ Message Queues --------
key        msqid      owner      perms      used-bytes   messages
0x00001f4f 0          Ruby       666        0            0

------ Shared Memory Segments --------
key        shmid      owner      perms      bytes      nattch     status

------ Semaphore Arrays --------
key        semid      owner      perms      nsems

获取ipc对象列表

/proc/sysvipc

cat /proc/sysvipc/msg
       key      msqid perms      cbytes       qnum lspid lrpid   uid   gid  cuid  cgid      stime      rtime      ctime
      8015          0   666           0          0     0     0  3000  3000  3000  3000          0          0 1598235273

查看限制

ipcs -l

------ Messages Limits --------
max queues system wide = 963
max size of message (bytes) = 8192
default max size of queue (bytes) = 16384

------ Shared Memory Limits --------
max number of segments = 4096
max seg size (kbytes) = 0
max total shared memory (kbytes) = 18014398442373116
min seg size (bytes) = 1

------ Semaphore Limits --------
max number of arrays = 32000
max semaphores per array = 32000
max semaphores system wide = 1024000000
max ops per semop call = 500
semaphore max value = 32767

消息队列

无法结合内核本身的文件描述符系统

标识符引用,键(key)复杂度都省

无连接,涉及到资源管理就会很糟糕

  • 什么时候删除?
  • 应用怎么保证不用的资源被删除?

避免使用system v ipc消息队列

信号量

进程同步 尤其是共享内存

共享内存

所处位置

全是偏移


内存映射

mmap munmap

支持映射文件

  • 省一步写,能快点

虚拟内存操作

mprotect mlock mincore

madvise 内存使用建议


posix ipc

消息队列 信号量 共享内存 用fd管理

信号量和共享内存api和system v的api差不多。都挺复杂


文件加锁

加锁与stdio缓冲问题

flock

fcntl


socket

创建fd(socket) 绑定fd(bind) 监听(listen, backlog含义,系统未accept之前的客户端connect占用的fd最大个数)

接受connect( accept, 返回连接的fd,操作这个fd进行通信)

客户端主动发起connect(失败?)

close没啥说的,得结合时序图才有意思

unix domain socket 本机通信用

tcp/ip

数据链路层隐藏

网络层无连接不可靠

传输层

TCP

  • 数据打包成段
  • 确认重传以及超时
  • 排序
  • 流量控制
    • 拥塞控制:慢启动和拥塞避免算法

UDP 注意分段

网络相关

  • 网络字节序, 大端
  • 主机服务转换函数
    • gethostbyname废弃,inet_ntop getaddrinfo

服务设计

  • 迭代型
  • 并发性
    • 预先准备好线程/进程 服务池
    • 集群
    • IO复用怎么没提?

inetd

高级主题

  • 部分读部分写
  • shutdown关闭一半
  • 深入TCP
    • 报文格式
    • 确认机制
    • 状态机
    • 建立和终止
      • listen被动打开
      • connect主动打开
      • close主动关闭,另一侧也执行close,被动关闭
    • TIME_WAIT
      • 可靠的断开,2MSL

netstat -a –inet

tcpdump抓流量

  • socke选项
    • SO_REUSEADDR

其他IO模型

水平触发和边缘处罚 LT ET

select poll是水平触发,信号驱动IO是边缘触发,epoll都支持

水平触发可以任意时刻查看fd的就绪状态,处理不完继续处理

边缘触发一次就得处理完,采取边缘触发的程序设计规则

  • 接收到IO时间尽可能多的执行IO,如果没这么做可能会失去至此那个的机会,数据丢失程序阻塞
    • 也就是说这种动作可能会饿到fd,比如上一组动作没处理完,这一组又在等待
  • 如果程序采用循环来处理fd尽可能多,而fd优势阻塞的,这样整个IO调用就阻塞住了,所以必须要改成非阻塞模式,有事件就重复执行IO直到失败为止
    • 对于epoll而言是这样的
      • 所有fd非阻塞
      • epoll_ctl管理fd列表
      • 循环
        • epoll_wait拿到就绪fd
        • 不断执行执行IO系统调用(read/write/send/accept/recv)直到EAGAIN到EWOULDBLOCK
  • 针对其他的fd会饿到的风险场景
    • 用一个列表维护一下有过就绪态的fd,把他们的超时时间调小(分给他们的时间片调小)
    • 维护的列表中已经出现过的,操作要稍微调度一下,rr之类的,而不是直接处理epoll_wait返回的列表,如果出现了错误,就移除

self-pipe技术。不多数


终端, 伪终端

CR EOF DISCARD

stty命令


ref
  1. https://www.veaxen.com/fork%E5%AF%B9%E8%A1%8C%E7%BC%93%E5%86%B2%E5%8C%BA%E7%9A%84%E5%BD%B1%E5%93%8D.html

  2. https://coolshell.cn/articles/7965.html

  3. mmap实现cp https://stackoverflow.com/questions/27535033/copying-files-using-memory-map


Read More

(译)终极python调试指南 Ultimate Guide to Python Debugging

必须打日志

不打日志必后悔,python设置日志非常简单

import logging
logging.basicConfig(
    filename='application.log',
    level=logging.WARNING,
    format= '[%(asctime)s] %(pathname)s:%(lineno)d %(levelname)s - %(message)s',
    datefmt='%H:%M:%S'
)

logging.error("Some serious error occurred.")
logging.warning('Function you are using is deprecated.')

有了日志logger能加上配置文件扩展(ini/yaml)就更好了(译者注:really?有点浮夸)

version: 1
disable_existing_loggers: true

formatters:
  standard:
    format: "[%(asctime)s] (pathname)s:%(lineno)d %(levelname)s - %(message)s"
    datefmt: '%H:%M:%S'

handlers:
  console:  # handler which will log into stdout
    class: logging.StreamHandler
    level: DEBUG
    formatter: standard  # Use formatter defined above
    stream: ext://sys.stdout
  file:  # handler which will log into file
    class: logging.handlers.RotatingFileHandler
    level: WARNING
    formatter: standard  # Use formatter defined above
    filename: /tmp/warnings.log
    maxBytes: 10485760 # 10MB
    backupCount: 10
    encoding: utf8

root:  # Loggers are organized in hierarchy - this is the root logger config
  level: ERROR
  handlers: [console, file]  # Attaches both handler defined above

loggers:  # Defines descendants of root logger
  mymodule:  # Logger for "mymodule"
    level: INFO
    handlers: [file]  # Will only use "file" handler defined above
    propagate: no  # Will not propagate logs to "root" logger
import yaml
from logging import config

with open("config.yaml", 'rt') as f:
    config_data = yaml.safe_load(f.read())
    config.dictConfig(config_data)

logger不直接支持yaml,但是可以把yaml转成dict,然后就可以用了

日志装饰器

有了日志还不够,发现了有问题的代码片,想看内部细节调用,直接上日志装饰器,这样要比直接在日志内加要方便的多 ,具体的细节就调节配置文件中的日志级别就可以了

from functools import wraps, partial
import logging

def attach_wrapper(obj, func=None):  # Helper function that attaches function as attribute of an object
    if func is None:
        return partial(attach_wrapper, obj)
    setattr(obj, func.__name__, func)
    return func

def log(level, message):  # Actual decorator
    def decorate(func):
        logger = logging.getLogger(func.__module__)  # Setup logger
        formatter = logging.Formatter(
            '%(asctime)s - %(name)s - %(levelname)s - %(message)s')
        handler = logging.StreamHandler()
        handler.setFormatter(formatter)
        logger.addHandler(handler)
        log_message = f"{func.__name__} - {message}"

        @wraps(func)
        def wrapper(*args, **kwargs):  # Logs the message and before executing the decorated function
            logger.log(level, log_message)
            return func(*args, **kwargs)

        @attach_wrapper(wrapper)  # Attaches "set_level" to "wrapper" as attribute
        def set_level(new_level):  # Function that allows us to set log level
            nonlocal level
            level = new_level

        @attach_wrapper(wrapper)  # Attaches "set_message" to "wrapper" as attribute
        def set_message(new_message):  # Function that allows us to set message
            nonlocal log_message
            log_message = f"{func.__name__} - {new_message}"

        return wrapper
    return decorate

# Example Usage
@log(logging.WARN, "example-param")
def somefunc(args):
    return args

somefunc("some args")

somefunc.set_level(logging.CRITICAL)  # Change log level by accessing internal decorator function
somefunc.set_message("new-message")  # Change log message by accessing internal decorator function
somefunc("some args")

实现__repr__ 方便打印

老生常谈了,实现__str__也行

class Circle:
    def __init__(self, x, y, radius):
        self.x = x
        self.y = y
        self.radius = radius

    def __repr__(self):
        return f"Rectangle({self.x}, {self.y}, {self.radius})"

...
c = Circle(100, 80, 30)
repr(c)
# Circle(100, 80, 30)

实现__missing__ 避免KeyError异常

如果实现了自己的dict(译者注:用到dict作为自己的内部成员,可以封装一层dict) 可以实现__missing__ 如果访问key key不存在,就会触发missing调用,帮助捕捉bug

调试崩溃的程序

过快崩溃来不及看日志? -i进入细节调用模式 python -i xx.py 如果这个细节还是不够用,可以调用pdb

# crashing_app.py
SOME_VAR = 42

class SomeError(Exception):
    pass

def func():
    raise SomeError("Something went wrong...")

func()
~ $ python3 -i crashing_app.py
Traceback (most recent call last):
  File "crashing_app.py", line 9, in <module>
    func()
  File "crashing_app.py", line 7, in func
    raise SomeError("Something went wrong...")
__main__.SomeError: Something went wrong...
>>> # We are interactive shell
>>> import pdb
>>> pdb.pm()  # start Post-Mortem debugger
> .../crashing_app.py(7)func()
-> raise SomeError("Something went wrong...")
(Pdb) # Now we are in debugger and can poke around and run some commands:
(Pdb) p SOME_VAR  # Print value of variable
42
(Pdb) l  # List surrounding code we are working with
  2
  3   class SomeError(Exception):
  4       pass
  5
  6   def func():
  7  ->     raise SomeError("Something went wrong...")
  8
  9   func()
[EOF]
(Pdb)  # Continue debugging... set breakpoints, step through the code, etc.

抓调用栈

import traceback
import sys

def func():
    try:
        raise SomeError("Something went wrong...")
    except:
        traceback.print_exc(file=sys.stderr)

重新加载模块,不用开启新shell

>>> import func from module
>>> func()
"This is result..."

# Make some changes to "func"
>>> func()
"This is result..."  # Outdated result
>>> from importlib import reload; reload(module)  # Reload "module" after changes made to "func"
>>> func()
"New result...

不必陷入编辑临时文件,执行,再改,再执行的循环

ref

  1. https://martinheinz.dev/blog/24
  2. 日志装饰器这个是装饰器典型用法了,但是却没真正用到代码里,真是惭愧啊
  3. 作者还推荐 https://remysharp.com/2015/10/14/the-art-of-debugging,不过我打不开这个网址,改天看吧


Read More

嵌套lambda 捕获shared_ptr引发的bug

bug代码片抽象成这样

#include <iostream>
#include <cstdlib>
#include <string>
#include <functional>
#include <vector>
#include <algorithm>
#include <thread>
#include <memory>
using namespace std;
int main()
{

    std::vector<shared_ptr<int>> v;
    for(int i=0;i<10; i++)
        v.push_back(make_shared<int>(i));
    std::vector<std::thread> threads;
    std::for_each(v.begin(), v.end(),
                  [&](shared_ptr<int> p) {
                  //改成传const引用就没问题了
                  //[&](const shared_ptr<int>& p) {
                    threads.emplace_back(std::move(std::thread([&]() {
                      std::cout << *p<< '\n';
                    })));
                  });
    
    
    /*for (auto &e : v) {
         threads.emplace_back(std::move(std::thread([&]() {
                      std::cout << *e << '\n';
                    })));
    }*/
    for (auto &t :threads)
        t.join();
}

问题出在内部的lambda是捕获引用,但是捕获的值是栈上的,这个栈上分配的值在该场景下是复用的,结果有问题。

嵌套lambda,一定要注意捕获参数。参考链接1有详细的介绍,总结五个常见的嵌套lambda搭配

  1. 传值,捕获值,没问题,但是会有赋值开销
  2. 传值,捕获引用,有问题,传的值是分配在栈上的,捕获引用可能会变,可能不存在等等
  3. 传引用,捕获引用,没问题,但是不能传右值
  4. 传const引用,捕获引用,没问题,但是传的值失去了左值能修改的特性
  5. 传值,移动捕获值,unique_ptr只能这么捕获。也是一个好的捕获方案,省一次拷贝。

上面的案例就犯了2 的错误。改成const 引用就好了

ref

  1. http://lucentbeing.com/writing/archives/nested-lambdas-and-move-capture-in-cpp-14/

  2. 关于lambda嵌套。很复杂。https://zh.cppreference.com/w/cpp/language/lambda

  3. 一个lambda的分析 https://web.mst.edu/~nmjxv3/articles/lambdas.html


Read More

erlang程序设计中文版读书笔记

个人读书笔记,我看书走马观花,侧重点比较诡异,大多流水记录,可读性不大。

入门

  • erl shell,执行最后要加个 .
  • 变量不变, 强制大写开头
  • = 表示模式匹配,不是常规语言中的赋值
    • 变量多次赋值,就会exception error: no match of right hand side value
  • 抛弃副作用,拥抱并行化
  • erlang中原子,表示非数字常量 (很诡异的歧义,注意)
    • 小写字母开头可以用_@连接
  • 每个表达式都必须有值
  • 元组与元组展开。和python c++类似
  • 列表,以及列表头尾快捷操作(有点像模板元)
  • 字符串
    • 就是整数列表,必须双引号,需要大写字母开头
    • $i表示字符i

顺序型编程

  • 一切都是表达式,一切表达式都有值

    • 所以说ruby perl rust那种最后留一个值做返回值的做法算是函数式风格的求值?
  • 模块 和python/js类似 ,-module(module_name) -export(func_name) 导出 -import(module_name, [func_name])
  • 函数
    • 同名不同目,也就是重载支持,可以,只做helper
    • func,也就是lambda,支持作为参数以及返回
  • 控制流对应成匹配,循环,那就用列表生成
  • if/case都是case模式匹配

  • 异常

    • try-catch 也是case模式的,丢失细节
    • erlang:error
    sqrt(X) when X < 0 ->
        erlang:error({squareRootNegativeArgument, X});
    sqrt(X) ->
        math:sqrt(X)
    

    有点像c++ 的constraint

  • 调用栈

    • erlang:get_stacktrace()
  • BIT 内建函数 buildin

    • tuple_to_list
    • 二进制流 «“cat”»
  • 属性-SomeTag()

  • 块表达式,begin end包起来整体求值

  • 宏,引用,这套和c有点像

  • 进程字典,每个进程有自己的私有数据

    • 运行时别用。当成元数据保存还行
  • 引用,全局唯一的值。有点像全局变量。

  • 下划线变量 用完就扔,调试用

编译运行

  • 指定搜索路径

    @spec code:add_patha(Dir) => true |{error, badd_directory}
    @spec code:add_pathz(Dir) => true |{error, badd_directory}
    
  • 编译运行与脚本运行

    • 脚本需要main入口,编译直接调用函数就行
    • 也得写makefile(2020了还手写吗)

并发

  • 并发原语

    • spawn启动一个进程,返回pid

    • a ! b表示发送

    • receive 可以带超时限制
      • 同步?
    • 进程管理 -> 注册进程

    • 一个echo

      start() ->
          spawn(fun() -> loop([]) end).
          
      rpc(Pid, Request) ->
          Pid ! {self(), Request},
          receive
              {Pid, Response} ->
                  Response
          end.
          
      loop(X) ->
          receive
              Any ->
                  io:format("Received:~p~n", [Any]),
                  loop(x)
          end.
      
      • 用到了尾递归技术,不能有多余的函数调用,不然不能复用栈空间、
  • MFA 启动进程, spawn(Mod, FuncName, Args) 动态更新升级?

并发编程中的错误处理

  • 链接进程

    进程依赖的强绑定,强制信号通知,这些东西在c是不强制的

  • on_exit 这东西c++也有,erlang的on_exit还可以hook已有 的进程,很妙

  • 天生分布式,进程的错误托管给别的进程来处理

  • 错误处理的三种基础盖面

    • 进程链接
    • 退出信号
    • 系统进程信息
  • 捕获退出的编程模式

    • 不在乎进程是否崩溃 Pid = spawn(func() -> ...end)
    • 进程崩溃我也崩溃,进程链接 Pid = spwan_link(fun() -> ...end)
    • 进程崩溃我处理错误

       ...
       process_flag(trap_exit, true),
       Pid = spwan_link(fun() -> ...end),
       ...
       loop(...).
      

    loop(State) -> receive {‘EXIT’, SomePid, Reason} -> %% do something with the error loop(State); … end

      
    
  • 错误处理原语

    • spawn_link
    • process_flag
    • link
    • unlink
    • exit
  • 工人-监工模型

  • monitor,守护进程

    keep_alive(Name, Fun) ->
      register(Name, Pid = spawn(Fun)),
      on_exit(Pid, fun(_Why) -> keep_alive(Name, Fun) end).
    

分布式编程

  • rpc
  • global名称注册函数/分布式锁定功能
  • cookies设置

基于socket

接口技术

  • open_port

文件编程

没啥说的

套接字编程

没啥说的

ETS/DETS

OTP

监控树和supervisor

多核问题

  • 状态不可变,并行化
  • MapReduce

更有效率

  • 大量进程 但不要太多
  • 避免副作用
  • 避免顺序瓶颈
  • 少量消息,大量计算

ref

  1. 关于erlang进程系统的资料

    1. https://www.iteye.com/blog/jzhihui-1482175
    2. https://www.erlang-factory.com/upload/presentations/558/efsf2012-whatsapp-scaling.pdf
    3. https://blog.yufeng.info/archives/2615
    4. https://www.zhihu.com/question/24732869
  2. 希望能看懂https://github.com/cbd/edis/blob/master/src/edis_sup.erl

  3. https://www.cnblogs.com/me-sa/ 这人写的erlang博客不错,讲真看这本书不如看他的博客


Read More

(译)Things I Wished More Developers Knew About Databases

翻译整理自 https://medium.com/@rakyll/things-i-wished-more-developers-knew-about-databases-2d0178464f78

You are lucky if 99.999% of the time network is not a problem.没有网络问题你就走大运了兄弟

一个数据,With 99.999% service availability, Google cites only 7.6% of Spanner (Google’s globally distributed database) issues are caused by networking even though it keeps crediting its dedicated networking as a core reason behind its availability.

ACID has many meanings. 正确理解ACID

ACID实现麻烦,NoSQL部分甚至没有ACID,在实现ACID上也有各种妥协,比如mongo的实现

提交落盘是个很重的操作,为了写性能高一些,就会牺牲一点D

Each database has different consistency and isolation capabilities. 每种数据库有自己的一致性和隔离策略

上面说了D,关于CI,实现也是各有取舍。根据CAP定律,C和A之间互有取舍(译者注:CAP的C和ACID的C不是一回事儿,但是这里说的consistency好像是一个事儿)

隔离级别实现程度不同,使用者按照自己的接受方案来做取舍

SQL标准的四个隔离级别,串行SI,可重复读RR,读提交RC,脏读RU,越严格代价约大,一般也就RC RR采用

https://github.com/ept/hermitage 这个文档介绍了不同隔离界别下数据库设计的实现场景

Optimistic locking is an option when you can’t hold a lock.当你拿不到所,就用乐观锁

数据库的锁是很重的,引入竞争,且要求数据一致性,排到锁所可能会受到网络分区的影响(译者注:为什么)也可能导致死锁,不好识别和解决,所以乐观锁就出现了,先更新,再检查版本,拿锁,拿不到,算了。

There are anomalies other than dirty reads and data loss. 有比脏读和丢数据更异常的场景

写偏序,没脏读,没数据丢失,但是结果就是不对

识别写偏序是很困难的。串行化,schema设计,数据库限制来约束写偏序

My database and I don’t always agree on ordering.数据库和我在时序上有不同的意见

用事务

Application-level sharding can live outside the application.应用级别分片可以脱离于应用本身

分片是水平扩展的放水阀,应用层分片也就是架构师/开发能预测数据怎么增长,能很好的水平扩展分片

其实想说就是分片在数据库上层,比如mycat这种,作者给的例子是vitess

AUTOINCREMENT’ing can be harmful. 自增ID可能有害

主键自增id在一些场景下的缺陷

  • 分布式数据库系统,维护全局自增代价过高
  • 根据主键做分片,自增id可能导致写偏序,热点分布不均匀
  • 主键更有意义,更唯一话,击中更快一些(译者注?这个影响会很大吗?)

主键自增ID和UUID索引在某些场景下都是个坏主意,要注意

Stale data can be useful and lock-free.过期数据可能有用且无锁

这里指的MVCC以及对应的快照实现,轻便无锁

  • 主动淘汰 vacuum
  • spanner有gc (译者注:rocksdb也有gc)

Clock skews happen between any clock sources. 什么时钟源都会有时钟偏序

时间同步,ntp服务gps时钟原子钟等等

google spanner是怎么保持整体时间有序的? TrueTime

Latency has many meanings.延迟的多种含义

对于数据库来说,延迟包含两部分,数据库本身延迟和客户端网络延迟,分析延迟相关问题以及指标时,要注意这两种

Evaluate performance requirements per transaction. 评估tps性能

要从典型场景考虑,比如

  • 插入一定条数数据的写吞吐和延迟
  • 查表的延迟。。。

这属于POC了吧,总之要考虑性能能不能满足自身业务的需求,并且收集指标信息和日志来分析

关于延迟的一些诊断方法,见https://medium.com/observability/want-to-debug-latency-7aa48ecbe8f7

Nested transactions can be harmful. 嵌套事务有害

Transactions shouldn’t maintain application state. 事务不应该维护应用的状态

指的是事务和数据共享竞争导致的数据错误

Query planners can tell a lot about databases.查询计划告诉你数据库的信息

就是看是不是全表扫描还是索引扫描了 explain

Online migrations are complex but possible.在线迁移复杂但也不是不可能

  • 双写数据库
  • 两个db都可读
  • 新db接管主要的读写
  • 停止写入旧db,只读,这时在新db上的读可能失败,因为数据不全,有一部分新数据没有
  • 同步所有数据,新写的新数据和旧数据

这是粗糙概括,参考文章https://stripe.com/blog/online-migrations

Significant database growth introduces unpredictability.数据库显著增长带来更多的不可预测问题

扩容了,即使你了解你的数据库,但还是可能会有不可预测的热点,不均匀的数据粉扑,硬件问题,增加的流量带来的网络分区问题等等,让你重新考虑你的数据模型,部署模型等等


Read More

回调lambda引发的shared_ptr循环依赖以及解决办法

参考链接资料汇总

如果你是搜索到这里的,请注意这个问题描述的场景

  • 对象自身的回调捕获了自己的值,自己造成了循环

出事的代码是这样的

class my_class {
// ...
public:
  typedef std::function<void()> callback;
  void on_complete(callback cb) { complete_callback = cb; }
private:
  callback complete_callback;
// ...
};
 
// ...
  std::shared_ptr<my_class> obj = std::make_shared<my_class>();
  obj->on_complete([obj]() {
    obj->clean_something_up();
  });
  executor->submit(obj);
// ...

注意,lambda默认是const,不改变值 SO

lambda的本质就是传值的一个指针,如果这里用std::function保存,会导致这个捕获的指针泄漏,导致这个指针永远不释放

解决方案1,weak_ptr,或者weak_from_this


class my_class {
// ...
public:
  typedef std::function<void()> callback;
  void on_complete(callback cb) { complete_callback = cb; }
private:
  callback complete_callback;
// ...
};
 
// ...
  std::shared_ptr<my_class> obj = std::make_shared<my_class>();
  std::weak_ptr<my_class> weak_obj(obj);
 
  obj->on_complete([weak_obj]() {
    auto obj = weak_obj.lock();
    if (obj) {
      obj->clean_something_up();
    }
  });
  executor->submit(obj);

解决方案2 ,lambda加上 mutable,见这个SO

参考链接2给出了一个比较经典的循环

void capture_by_value(uvw::Loop &loop) {
    auto conn = loop.resource<uvw::TcpHandle>();
    auto timer = loop.resource<uvw::TimerHandle>();

    // OK: capture uses [=]
    conn->on<uvw::CloseEvent>([=](const auto &, auto &) {
        timer->close();
    });

    // Now timer has a callback to conn, and vice versa...
    timer->on<uvw::CloseEvent>([=](const auto &, auto &) {
        conn->close();
    });
};

这里conn和timer循环了,如何规避?weak_from_this(前提,继承std::enable_shared_from_this.)

void capture_weak_by_value(uvw::Loop &loop) {
    auto conn = loop.resource<uvw::TcpHandle>();
    // Create a std::weak_ptr to the connection. weak_from_this() is new
    // in C++17, and is enabled on all classes that inherit from
    // std::enable_shared_from_this.
    auto w_conn = conn->weak_from_this();

    auto timer = loop.resource<uvw::TimerHandle>();
    auto w_timer = timer->weak_from_this();  // as above

    // OK, uses weak_ptr
    conn->on<uvw::CloseEvent>([=](const auto &, auto &) {
        if (auto t = w_timer.lock()) t->close();
    });

    // OK, uses weak_ptr
    timer->on<uvw::CloseEvent>([=](const auto &, auto &) {
        if (auto c = w_conn.lock()) c->close();
    });
});

ref

  1. http://web.archive.org/web/20180324083405/http://https://floating.io/2017/07/lambda-shared_ptr-memory-leak/ 原网址挂了
  2. https://eklitzke.org/notes-on-std-shared-ptr-and-std-weak-ptr 介绍weak_from_this

Read More

Optimizing Bulk Load in RocksDB

还是rockset的文章,讲他们怎么优化批量载入rocksdb的速度

几个优化

在延迟和吞吐之间的取舍

批量加载的时候,调高writebatch大小,其他场景,writebatch不要太大。

并发写

正常的对数据库的操作,保证只有一个写线程。这样不会有多线程写入阻塞问题。怕影响查询操作延迟,但对于这种加载场景,不需要考虑查询操作影响,把writebatch分配到不同的写线程做并发写,注意,要考虑共享的数据,尽可能让writebatch之间不影响不阻塞

不写memtable

构造的数据直接调用 IngestExternalFile() api,(rocksdb文档见这里) 避免写入memtable来同步memtable,这个动作速度快,且干净

但是有局限,这样构造,sst文件只有一层,如果有零星的大sst文件,后台compaction会非常慢。解决方法,一个writebatch写成一个sst文件。

停掉compaction

RocksDB Performance Benchmarks. 官方也建议,使用批量加载最好先停掉compaction然后最后做一次大的compaction,这样避免影响读写,但是

  • sst文件增多,点查很慢,加上定制bloomfilter有所改善可是查bloomfilter开销也很大
  • 最后一次compaction通常是单线程来做,虽然可以通过 max_subcompactions来改,但是效果有限,因为只有一层文件,文件是有重叠的,compaction算法找不到合并区间,所以最后还是一个线程遍历来搞,解决办法,手动对几个小范围做CompactFiles().,生成不是L0层的文件,这样就有区间,就能并发compaction了。前文2提到,他们L1 L0是不压缩的 (为什么压缩会影响写速率?)
结论

在这些优化前提下,加载200g未压缩数据(Lz4压缩后80g) 需要52分钟(70MB/s 18核)初始化加载用了35分钟,最后compaction用例17分钟,如果没有这些优化需要18小时,如果只增加writebatch大小以及并发写线程,用了5个小时

所有试验,只用了一个rocksdb实例

ref
  1. https://rockset.com/blog/optimizing-bulk-load-in-rocksdb/

  2. https://rockset.com/blog/how-we-use-rocksdb-at-rockset/

  3. https://rocksdb.org/blog/2017/02/17/bulkoad-ingest-sst-file.html


Read More

sqlite3资料整理


源码注释 https://github.com/HuiLi/Sqlite3.07.14

在线文档https://huili.github.io/

写一个sqlite 简单版本 https://github.com/madushadhanushka/simple-sqlite


Read More

记录几个数据指标用于估算


v2-0bca913bed8f7d40ac523dbb7688da07_720w

Google的Jeff Dean给的一些数据(一个talk的ppt, “Designs, Lessons and Advice from Building Large Distributed Systems” 23页),可以看到1Gbps的网络比硬盘的bandwidth高了很多,记住这些数据对设计高性能系统和对系统的性能估算很有帮助。

L1 cache reference 0.5 ns

Branch mispredict 5 ns

L2 cache reference 7 ns

Mutex lock/unlock 25 ns

Main memory reference 100 ns

Compress 1K bytes with Zippy 3,000 ns

Send 2K bytes over 1 Gbps network 20,000 ns

Read 1 MB sequentially from memory 250,000 ns

Round trip within same datacenter 500,000 ns

Disk seek 10,000,000 ns

Read 1 MB sequentially from disk 20,000,000 ns

Send packet CA->Netherlands->CA 150,000,000 ns

ref

https://www.zhihu.com/question/47589908


Read More

rockset是如何使用rocksdb的


20211210 他们的rocksdb开源了。有时间看看 https://github.com/rockset/rocksdb-cloud

rockset 是一个db服务提供商,他们用rocksdb来实现converged indexing 我也不明白是什么意思,在参考链接2有介绍,大概就是有一个文档,保存成行,成列,成index,他们大量采用的rocksdb

架构图是这样的

用户创建一个表会分成N个分片,每个分片有两个以上副本,每个分片副本放在一个rocksdb 叶节点上,每个叶节点有很多表的众多副本(他们的线上环境有一百多个),一个叶节点的一个分片副本有一个rocksdb实例,更多细节看参考链接34

下面是他们的优化手段

rocksdb-cloud

rocksdb本身是嵌入式数据存储,本身不高可用,Rockset做了rocksdb-cloud,基于S3来实现高可用

禁止WAL

架构本身有分布式日志存储来维护日志,不需要rocksdb本身的wal

Writer Rate Limit 写速率

叶节点接受查询和写,rockset能接受/ 容忍大量写导致查的高延迟,但是,还是想尽可能的让查的能力更平稳一些,所以限制了rocksdb实例的写速率,限制了并发写的线程数,降低写导致的查询延迟

在限制写的同时,也要让LSM 更平衡和以及主动触发rocksdb的stall机制,(?rocksdb原生没有,rockset自己实现的。rockset也要实现从应用到rocksdb端的限流

Sorted Write Batch

如果组提交是排好序的,并发写会更快,应用上层写的时候会自己排序

Dynamic Level Target Sizes

涉及到rocksdb compactiong策略,level compaction,本层文件大小没达到上限是不会做compact的,每层都是十倍放大,空间放大非常恐怖,见参考链接5描述,为了避免这个,上限大小编程动态的了,这样避免空间放大

AdvancedColumnFamilyOptions::level_compaction_dynamic_level_bytes = true
Shared Block Cache

这个是经验了,一个应用内,共用同一个blockcache,这样内存利用更可观

rockset使用25% 的内存来做block cache,故意留给系统page cache一部分,因为page cache保存了压缩的block,block cache保存解压过的block,page cache能降低一点系统读压力

参考链接6的论文也有介绍

L0 L1层不压缩

L0 L1层文件compact带来的优势不大,并且 L0 compact到L1层需要访问L1的文件,范围扫描也利用用不上L0的bloom filter 压缩白白浪费cpu

rocksdb 团队也推荐L0 L1不压缩,剩下的用LZ4压缩

bloom filter on key prefix

这和rockset的设计有关, 每个文档的每个字段都保存了三种方式(行,列,索引),这就是三种范围,所以查询也得三种查法,不用点查,用前缀范围查询,所以 BlockBasedTableOptions::whole_key_filtering to false,这样bloomfilter也会有问题,所以定制了ColumnFamilyOptions::prefix_extractor,针对特定的前缀来构造bloom filter

iterator freepool 迭代器池子

大量的范围查询创建大量的iterator,这是很花费性能的,所以有iterator 池,尽可能复用

综上,配置如下

Options.max_background_flushes: 2
Options.max_background_compactions: 8
Options.avoid_flush_during_shutdown: 1
Options.compaction_readahead_size: 16384
ColumnFamilyOptions.comparator: leveldb.BytewiseComparator
ColumnFamilyOptions.table_factory: BlockBasedTable
BlockBasedTableOptions.checksum: kxxHash
BlockBasedTableOptions.block_size: 16384
BlockBasedTableOptions.filter_policy: rocksdb.BuiltinBloomFilter
BlockBasedTableOptions.whole_key_filtering: 0
BlockBasedTableOptions.format_version: 4
LRUCacheOptionsOptions.capacity : 8589934592
ColumnFamilyOptions.write_buffer_size: 134217728
ColumnFamilyOptions.compression[0]: NoCompression
ColumnFamilyOptions.compression[1]: NoCompression
ColumnFamilyOptions.compression[2]: LZ4
ColumnFamilyOptions.prefix_extractor: CustomPrefixExtractor
ColumnFamilyOptions.compression_opts.max_dict_bytes: 32768

ref

  1. https://rockset.com/blog/how-we-use-rocksdb-at-rockset/
  2. https://rockset.com/blog/converged-indexing-the-secret-sauce-behind-rocksets-fast-queries/
  3. https://rockset.com/blog/aggregator-leaf-tailer-an-architecture-for-live-analytics-on-event-streams/
  4. https://www.rockset.com/Rockset_Concepts_Design_Architecture.pdf
  5. https://rocksdb.org/blog/2015/07/23/dynamic-level.html
  6. http://cidrdb.org/cidr2017/papers/p82-dong-cidr17.pdf

Read More

^