惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

阮一峰的网络日志
阮一峰的网络日志
D
Darknet – Hacking Tools, Hacker News & Cyber Security
S
Schneier on Security
The Last Watchdog
The Last Watchdog
Cyberwarzone
Cyberwarzone
S
Securelist
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
C
Cyber Attacks, Cyber Crime and Cyber Security
L
Lohrmann on Cybersecurity
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
博客园 - 司徒正美
The Cloudflare Blog
V
V2EX
博客园_首页
博客园 - 聂微东
Vercel News
Vercel News
人人都是产品经理
人人都是产品经理
G
GRAHAM CLULEY
T
Tenable Blog
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
L
LINUX DO - 最新话题
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
SecWiki News
SecWiki News
博客园 - 三生石上(FineUI控件)
S
Secure Thoughts
N
News | PayPal Newsroom
T
The Blog of Author Tim Ferriss
The GitHub Blog
The GitHub Blog
T
Troy Hunt's Blog
博客园 - 【当耐特】
Forbes - Security
Forbes - Security
H
Hacker News: Front Page
A
About on SuperTechFans
B
Blog RSS Feed
Engineering at Meta
Engineering at Meta
MongoDB | Blog
MongoDB | Blog
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
D
DataBreaches.Net
P
Privacy & Cybersecurity Law Blog
Schneier on Security
Schneier on Security
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google DeepMind News
Google DeepMind News
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Jina AI
Jina AI
D
Docker
P
Proofpoint News Feed

TinyEdi

Coroutine GDB with Python Bazel Notes Shared Library I dreamt for so long The Building Blocks of Transformers A note for cmake A Strory of Mixin Tablegen Language Tutorial Lit and FileCheck 比较运算符, Min, Max, Sort 和 Order IEEE 754 的 inf 比较问题 KD树与SKD树 汉诺塔问题-记录
Unix related things
Edimetia3D · 2024-02-19 · via TinyEdi
记录

Edimetia3D

21 min read

这是一篇2017年左右的记录, 仅用作分享

  • 在shell内能干的事,我们都可以比较简单地通过系统调用实现.
  • `称为反引号,^称为脱字符,常用来表示CTRL
  • windows的系统调用是不开放的,windows下只能直接使用windows.h里的windows API.
  • /dev目录下的设备是供用于程序直接使用的,主要由block,char,pipe,socket类型
    • 并不是所有设备都能映射为这种形式
  • /sys/device/目录称为sysfs,他下面存放了所有设备的信息.(不能直接从/dev获得任何设备信息)
    • udevadm info --query=all --name="/dev/sda1"可以用于查询/dev下某个设备对应的sysfs路径

      权限系统

  • 权限系统由两部分组成
    • 文件属性:用于标注文件owner,所属组,以及权限的设定(默认只有owner和root可以修改权限设置)
    • 运行时检查系统:每个进程都有一个"有效ID",该"有效ID"将按UGO的顺序与文件的权限设置进行匹配.
      • 若进程有效ID == 文件的owner ID: 则按U测试权限
      • 否则,若进程有效ID == 文件的group ID:责任按G测试权限
      • 否则,按O测试权限
  • 进程有三组ID:实际ID,有效ID,save_ID. 当进行权限裁定时,是按照有效ID进行的.
    • 进程的实际ID就是caller的ID,caller ID 本质上在登陆时决定.
    • 进程的save_ID是父进程的有效ID
    • 有效ID默认情况和父进程的有效ID相同(对用户而言,父进程一般是shell),如果可执行文件设置了suid,那么就按可执行文件的来。
    • 用户可以主动将save_id及实际ID的任意一个设置为有效ID
  • 权限系统相关的主要是安全问题,在应用和开发中一般不必特别关注.
  • 关于特殊权限
    • suid:chmod u+s当文件可执行时,执行进程的有效ID是文件的owner,而非caller.
    • sgid:chmod g+s在该目录中创建的文件默认都和目录属于同一个组,而非属于创建者的组.
    • sticky:chmod o+t对目录拥有写入权限的用户仅可以写用户自身的文件,无法写其他用户的文件.
    • 特殊权限是通过隐藏的3位二进制值实现的,这三位位于UGO前,换言之,一个文件的真实权限依12位存储,但是由于特殊权限不常用且功能特定,因此默认不显示.

典型目录结构

  • `/lib目录中一般只有共享库
  • /bin/sbin存放的是系统必须的可执行文件,这些程序一般都对应了系统调用.
    • /usr/bin/usr/sbin做的事一般都能在用户空间完成
    • /usr/local主要由管理员安装一些所有用户都要用到的软件
  • /var主要存储日志,/var/tmp不会被自动清空,/tmp会被自动清空
  • /etc主要存放配置文件
  • /opt里常用于存储一些公用的第三方软件
  • /boot内存放启动相关的文件,/vmlinuz/boot/vmlinuz内存放内核,可加载的内核模块在/lib/modules
  • /sys/devices/则用于展示设备路径,设备路径可以唯一的标识硬件,在linux中以目录的形式存在,目录中的文件可以提供设备信息
  • /dev下一般是经驱动映射出来的典型IO设备.

重定向与命令替换

  • func1 | func2 :将func1的标准输出输入到func2的标准输入
  • func2 $(func1):将func1的stdout作为命令行参数输入到func2
    • 和"func2 func1"效果相同
    • 和"func1 | xargs func2" 效果相同

进程

  • 0 号进程一般是OS内核,内核态. 1 号进程一般是init,在用户态运行;他们都是以uid 0执行
  • fork进程时,子进程并不是和父进程一毛一样,例如,对于线程的拷贝是未定义的,一般可以认为fork之后,只有调用fork的线程被保留了.
    • 多线程下的fork一定要先查下相关资料.
  • exit() 一般是一个库函数,_exit()一般对应了一个系统调用,前者会额外做一些Runtime相关的清理工作.
  • 进程的终止状态有两种:正常终止,异常终止,可以在waitpid()之后检查.
    • 子进程终止时,会向父进程发送一个SIGCHILD信号.
    • 在UNIX中,进程终止后进入僵尸模式,直到它的终止状态被父进程取走,内核才开始执行销毁.
    • 如果父进程先于子进程被销毁,那么所有子进程都会被过继给init进程,init将会及时的取走终止状态,销毁僵尸进程.
  • 父进程调用wait()waitpid()是非常重要的,因为只有这样才能获得子进程的终止状态.
    • 子进程暂停时,这两个函数也会返回相应的状态.
  • "终止状态值"和"退出状态值"是不同的,后者是指进程正常终止时通过returnexit()返回的指,退出状态值用不同的API获得.
  • exec并不会完全清除状态,仍有部分状态是从当前进程继承的.
    • 一般情况下,exec时会关闭所有的文件描述符,这种行为可以用fcntl控制.
  • exec时,路径path可以为脚本文件,此时OS会根据脚本第一行的指示启动解释器,然后执行解释器 path arg1 .... argn.
  • 一般而言,只有execve()(或fexecve())是系统调用,其他都是库函数.
  • argv[0] 通常对被调用进程没有特殊意义. 在bash中,如果shell是被init启动的,那么它的argv[0]被设置为/,这个bash会据此认为自己是"登录shell",之后就会加载一系列配置文件,非登录shell则不会这么做.
  • root用户设置id后,会直接修改有效ID/真实ID/save_id, 之后就再也回不到root了.
  • 如果对安全敏感,那么只要父进程的save_id有特权时,就一定要关注exec及system等启动新进程的操作.
  • longjmp 和 setjmp: 主要关注的是在回跳的是时候,是如何恢复现场的,有的实现中,是完整的栈展开会退到现场,有的则仅仅是jmp过去,再恢复现场的寄存器.
    • C++的异常一般就是通过setjmp和longjmp实现的, 在catch的位置会setjmp, 而在throw时,实际就是longjmp到最近set的位置.

文件IO

  • 文件IO时,lseek可以超出当前文件的末尾,以用于延长文件,此时可能会在文件末尾创造空洞.
    • 文件空洞仅仅是在存储上可以优化,它就是正常文件的一部分,可以认为空洞里面全是0.
  • 系统调用提供的IO都是"无缓冲"的,是指没有用户层的软件缓冲,OS内的软缓冲以及物理上的高速缓冲体系仍然是工作的.
    • OS一般会提供系统调用刷新缓冲.,unix中是fsync,sync和fdadasync.
      struct FileDescriptor{
      //文件描述符
      FileDsecriptor(){
      file= GetNewFile();
      }
      ...
      File * file
      } 
  • OS管理File对象;进程维护FileDescriptor对象;文件可以被重复打开;联系这三个属性,就能理解OS的行为
    • 进程中的int FD值相当于索引号.
    • 单个进程可以重复打开文件,获得多个描述符,可以用于同时IO文件的多个位置.
    • 尽管fork()会拷贝属于进程的描述符,但是里面的File * file指针仅仅是浅拷贝,父进程和子进程将使用同一个File * file
    • 多个进程可以重复打开文件,获得各自的描述符,可以用于同时IO文件.
    • 通过文件共享数据时,一定要考虑缓冲/锁/数据同步的问题.
  • O_APPEND文件标志是多进程安全的,它保证每次IO前都会先定位到文件末端.
    • 因此,除非必要,否则不要使用O_APPEND,这样你将失去在文件中任意位置写入的权利.
  • FileDescriptor的拷贝.
    • int new_fd=dup(old_fd);//在目前的最小控线位置创建old_fd的拷贝.
    • int new_fd=dup2(old_fd,at_fd);// 在at_fd索引位置创建old_Fd的拷贝
    • int new_fd=open("/dev/fd/0"),等价于dup(0),有的系统还提供了/dev/stdin,/dev/stdout等,用于辅助我们打开常用的设备
    • 拷贝的主要用途是重定向,例如dup2(fd,0)就能把标准输入覆盖.
  • fcntl提供了对文件描述符的相关操作,主要用于动态的修改参数
    • 例如,F_SETFD用于设置描述符,F_SETFL用于设置fd对应的文件表项目.
  • 标准IO库是基于fd开发的,但是标准库的fopen并不能打开所有文件,在POSIX中,可以先获得fd,再通过fdopen()创建对应的fp
  • 标准IO库的软件缓冲有三种模式:
    • 全缓冲:缓冲区满了才刷新
    • 无缓冲:禁用软件缓冲.
    • 行缓冲:在字符模式中,若遇到\n则刷新缓冲
    • ISO C中: stdin和stdout在不指向交互设备时,才可能是全缓冲的;stderr是无缓冲的.
    • 对每一个FILE * fp,都可以通过setvbuf()控制它的缓冲行为(改变缓冲尺寸,禁用缓冲等)
  • 注意: 标准IO库中,读和写共享一个缓冲空间,如果fflush了,那么所有没有被用户读走的数据都将被丢弃.
  • 字符流读取中的ungetc()一般仅操作软件缓冲,所以可逆/不可逆的流都可以使用.
  • 文件打开模式
    • +仅仅是保证了读写权限,不会改变行为,例如w+始终会截断文件.
  • IO支持"记录锁",可以对文件的部分字节加锁,保证并行安全,这种锁是跨进程OS级别的.(apue 14章)
    • 在进程内,还可以使用flockfile(FILE * fd)系列API,用于保护fd的锁
    • 记录锁分为"建议锁"和"强制锁","建议锁"要求所有进程都按带锁的模式编程,"强制锁"则由OS维护锁状态,任何IO函数都会校验锁状态.
  • 低速IO(阻塞式IO):可能永久阻塞进程的IO函数称为低速IO. 相反的概念为"非阻塞IO"
  • IO批量查询(IO多路转接):pool,select以及pselect
    • 批量查询功能可以一次查询多个fd的状态,从而判断其是否可读/可写/异常. 相比于手动轮询每个fd的状态, 这组API在等待期间可以让出CPU资源,效率更高.
      • readvwritev
    • readv:连续读取一个fd,把其中的值顺序存储到多个buf中
    • writev:顺序从多个buf中读值,再写入fd中
    • 这里面的多个buf形成了一个指针数组,也就是vector

进程间通信

为了可扩展性, 原则上进程间通信最好仅使用TCP Socket.(这另一方面还保证了不会出现跨进程的对象,自然也就避免了跨进程内存的同步问题)
从设计上说,如果你只共享少量的数据,拷贝开销很小,那么为什么要用共享内存呢? 但是如果你需要共享大量的数据(此时往往还需要知道内存的布局),为什么不用多线程呢?

管道

  • pipe():
    • 仅能单向传输,双向传输需要使用两个管道.
    • 在子进程和父进程之间可以建立"管道",体现出来就是父进程有一个fd1,子进程有一个fd2,一边read,另一边write来流式传递数据.
    • 管道两端有2个fd,从一边写入的只能由另一边读出
  • system执行新函数时,只能获得返回状态,而通常我们需要的是stdout,此时可以用popen,popen可以视为对pipe + fork + exec的封装
    • FILE * popen(const char * cmdstring, char * type)
    • cmdstring将产生一个新进程,该进程执行sh -c cmdstring
    • type可以是r或者w,这意味着返回的FILE * fd是可读或者可写的.
      • w模式启动后: 当前程序对fd的写入将对应子进程的标准输入.
      • r模式启动后: 当前进程对fd的读取将对应子进程的标准输出.
    • popen常用于拦截标准IO,用于做预处理/后处理
    • 特别的popen的父进程/子进程可以是我们自己编写的,这也就实现了一种单向通信的方法.
  • 具名管道FIFO
    • 现通过mkfifo/mknod在文件系统中创建一个FIFO类型的文件.
    • 程序按照自己的需求open这个文件
    • FIFO的fd既可以读,又可以写,按照队列的模式传输数据
    • FIFO有读端和写端的概念,只有当读端/写端都有fd打开时,对FIFO的读写才不会阻塞.
  • 在PIPE和FIFO中,PIPE_BUF用于描述原子传输的个数,当写端调用write时,只要尺寸小于PIPE_BUF,就可以一次写完.

XSI IPC

  • 包括:进程间消息队列,进程间信号量,进程间共享存储区. 尽管如此,一般只有"共享存储区"被认为是实用的
  • XSI IPC的共享存储区不需要文件作为中介,直接操作原始内存,不存在来回拷贝的开销,是性能最好的方案.在共享存储区中实现锁/信号量等对象,就能保证跨进程IO的安全.
  • XSI IPC的实现是和文件系统解耦的,所以不能通过文件系统进行管理,为此,文件IO的诸多特性就不能使用了
  • XSI IPC没有引用计数,必须由用户销毁.

POSIX 信号量

  • POSIX信号量是对 IPC 信号量的高性能改进.
    • 默认的POSIX信号量是未命名的,就是典型的信号量,用于线程间同步.(如果放在共享存储区,也可以跨进程同步)
    • 可以为信号量取名字,由于一些系统会使用文件系统实现,所以信号量的名字总应该看起来是合法的绝对路径,以保证可移植性,例如/mysem

套接字(Socket)

网络基础

  • TCP和UDP的下一层都是IP层,所以从本质上来说,TCP数据包和UDP数据包的区别并不大.
  • UDP协议可以认为是对IP层的简单封装,从而让用户能直接传输数据.在UDP中,将(source_ip,source_port,target_ip,target_port,data)封好之后,就可以直接交给IP层传输了;最终,目标机器的对应端口会收到数据包,然后目标再执行对应的解包操作.
    • 逻辑传输单位为"包": 发送方和接收方处理的都是整个包,需要手动解析包内容.
    • 无序: 由于网络拥塞等原因,UDP包到达的顺序和发出的顺序是不确定的.如果需要依赖包顺序,就需要在包的数据段中打上时间戳.
    • 不可靠: 由于网络拥塞等原因,UDP包可能会丢失,但是发送方/接收方无法直接检查是否丢失,为了保证可靠性,一方面需要在包中打上包索引这样的标记,另一方面需要在确定包丢失后,手动重新发包.
    • 无连接:从工作流中来说,发送方只负责把UDP包发送给IP层, 而不关注目标地址是否存在,也不关心数据是否到达. 所以说,UDP是无连接的.
    • 负载大: UDP包是需要无条件转发的,目标也需要无条件接收, 客户接收后,再主动决定是否需要丢弃;对网络和客户而言,都引入了一定的负载,容易引起网络拥塞问题.
  • TCP是可靠的,有序的,有连接的,且有拥塞控制.具体而言,TCP相当于在UDP的基础上实现了一个warpper, 一方面解决了一些问题,另一方面则引入了一些额外开销. 由于TCP的长期应用, 硬件层/OS层围绕TCP进行了很多优化,除非UDP的优势十分明显,一般情况都应该使用TCP.(事实上, 完全可以在UDP的基础上实现类似TCP的功能,但是何苦呢?)
    • 传输逻辑为"字节流", 对于发送方和接收方而言,处理更为便利.
    • 有连接: TCP中, 在初始阶段会有一个建立连接的过程, 这一过程的主要目的是确认连接双方彼此都存在. 在确认双方存在后,就认为连接建立了.(这种"连接"仅仅确认彼此存在,是比较弱的,在一些应用中,连接阶段过后,常常会互发心跳包,重复确认彼此存在)
    • 有序: TCP数据包的顺序由协议维护,保证数据的顺序
    • 可靠: 如果出现了丢包,TCP会自动重新发包
    • 负载小: OS的拥塞控制和硬件的QoS可以有效地控制TCP包在网络中的传输.

基础

  • 相比管道/共享存储区, socket的主要优势是可以使用网络协议,从而实现网络级别的进程间通信.socket当然也可以用于本机进程间的通信, 此时,与管道/共享内存相比,使用socket更容易在将来移植到网络环境中.

  • int socket(int domain,int type,int protocl)

    • 返回值: sockfd,该socket描述符暂时为空 (指向一个空的socket)
    • domain: 网络域,一般是INET(IPv4), INET6(IPv6),或UNIX,域不同时,使用的网络地址格式不同, 所以又常称为address family.
      • 如果是在本机进程之间使用socket,那么可以使用UNIX,其编程模型更为简单
    • type: 传输方式,一般是DGRAM,采用带有目标地址的定长数据包; 或 STREAM,流式数据传输
    • protocl: 使用0即可,采用与domain+type匹配的默认协议

    UNIX中,将行为模式typeprotocol解除了绑定,这是为了保留弹性,例如以后除了一个新的FAST_TCP,它的行为和TCP一致,但是速度更快,此时的行为模式是可以不变的,但是协议可以直接换掉. 在目前,一般情况下,可以认为DGRAM就是UDP, STREAM就是TCP

  • socket的典型工作模式为: host1_ <-> host1_sockfd <—->host2_sockfd <-> host2.

    • socket传输数据一定需要一对socket_fd,从一个fd写入的只能由另一个fd读出.
  • socketfd默认是空的, 需要通过bind来将其绑定到具体的socket file (如果对应的socket file不存在,bind 会创建一个新的), 具体而言,socket addresssocket file是一一对应的, 域内通过socket address来区别不同的socket实体

    • socket_file 由OS管理,用户不能直接open socketfile,只能通过bind后的sockfd来read/write 访问sockfile.
    • 对于INET和INET6,socket address的物理地址是IP:PORT型的
    • 对于客户端,一般不必显式的调用bind,可以直接由OS分配.
    • 不同协议对于socket file的使用有不同的规则, 例如, TCP中,如果一个sockfd已经listen了socket file,那么一般就不允许其他fd再listen这个socket file了 (并行安全相关)
  • sockfd既可以close,又可以shutdown, close只减少sockfile上的引用计数,shutdown则用于禁用在sockfile上的IO.

  • 套接字中的数据传输/网络地址都是大端存储的,传输时一般要处理端序问题.

套接字网络地址sockaddr

  • sockaddr实际上是一个"基类指针", 当你得到一个struct sockaddr * address时,首先需要获得位于头部的family值, 然后再根据它来进行一次指针类型的转换.

    • 一般而言,用户是不需要解析sockaddr的,只需要在用的时候传走就行了.
    • 对于INET和INET6,sockaddr里存储的主要是IP地址和端口号
      typedef unsigned short  sa_family_t;// AF_INET, AF_INET6, UNIX
      #define RESERVE_SIZE
      struct sockaddr{
      sa_family_t sa_family;
      char sa_data[RESERVE_SIZE] ;
      };
      struct sockaddr_in{
      sa_family_t sa_family;
      uint16_t port; 
      uint8_t addr[4];
      uint8_t zero[8]; //reserved
      };
      struct sockaddr_in6{
      sa_family_t sa_family;
      uint16_t port;
      uint32_t flowinfo;
      uint8_t addr[16];
      uint32_t scope_id;
      };
  • 开发中,主要通过getaddrinfo()来获取需要的sockaddr, 一方面可以改善可移植性,另一方面getaddrinfo确实很便利.

    int getaddrinfo(const char * host,
                const char * service,
                const addrinfo * hint,
                addrinfo * ret_info)
  • getaddrinfo()

    • host和service形式很多,典型的如("localhost","ftp",...)("www.zzz.com","9000",...)都可以
    • host和service至少需要提供一个,此时,将返回所有满足要求的ret_info
    • 返回的ret_info是一个链表的头,该链表通过freeaddrinfo()释放
    • ret_info->ai_addrret_info->ai_next是最常用的两个成员,前者是sockaddr *型的,可以给用户使用,后者是链表的next.
      • 输入的hint相当于额外的输入参数,主要用于过滤返回的ret_info

为sockfd分配网络地址,以及监听.

  • 对于大部分数据传输的API, socket库会自动为socketfd自动bind一个可用的地址. 如果自动地址不能满足需求,可以手动bind地址.
  • int bind(int sockfd,sockaddr* local_addr,socklen_t)
    • 这里, local_addr必须是位于本机的一个合法地址
    • 一般一个sockfd只能绑定到一个local_addr上,一个local_addr上可以绑定多个sockfd
  • 对于有连接的协议,服务端需要先listen,再accept,对于客户机,可以connect到阻塞在accept的服务端上
    • listen后,才可以调用accept()
    • accept用于等待外部的connect()请求(默认会阻塞线程),当和远端建立连接后,会返回一个新的sockfd_new, 原来的fd仍将服务于监听工作.

accept是负责实现TCP三次握手的部分; 远端先发信过来,报告远端的地址;本机再向远端发送一个socket_new的地址;远端再回复一个"收到", 连接就算建立完成了

  • 对于已经建立连接的fd,可以用getsocknamegetpeername来查找自身以及peer的addr.
  • 对于无连接的协议, connect也可以正常工作,此时,其逻辑功能是:为fd绑定一个远端地址,这样就可以避免sendto和recivefrom的调用,使用标准的read/write系统调用,简化开发流程.
  • UNIX域套接字有两个典型场景, 这是其他手段不易/不能实现的
    • 进程间传递fd: 在STREAM废除之后,只有UNIX域套接字可以在进程间传递打开了的文件描述符.
    • 进程间数据报:可以绕开网络驱动直接实现进程间可靠地数据报机制.
  • UNIX域套接字的地址需要按文件系统来定义.
    • UNIX域套接字会在bind时自动创建一个文件系统中的文件.
    • UNIX域套接字的地址只能被绑定一次,在绑定前,可以尝试在文件系统中删除对应的sock文件.
      struct sockaddr_un{
      sa_family_t family;
      char addr[MAX_LENGTH];