这是一篇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位存储,但是由于特殊权限不常用且功能特定,因此默认不显示.
- suid:
典型目录结构
- `/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" 效果相同
- 和"func2
进程
- 0 号进程一般是OS内核,内核态. 1 号进程一般是init,在用户态运行;他们都是以uid 0执行
- fork进程时,子进程并不是和父进程一毛一样,例如,对于线程的拷贝是未定义的,一般可以认为fork之后,只有调用fork的线程被保留了.
- 多线程下的
fork
一定要先查下相关资料.
- 多线程下的
exit()
一般是一个库函数,_exit()
一般对应了一个系统调用,前者会额外做一些Runtime相关的清理工作.- 进程的终止状态有两种:正常终止,异常终止,可以在
waitpid()
之后检查.- 子进程终止时,会向父进程发送一个SIGCHILD信号.
- 在UNIX中,进程终止后进入僵尸模式,直到它的终止状态被父进程取走,内核才开始执行销毁.
- 如果父进程先于子进程被销毁,那么所有子进程都会被过继给
init
进程,init
将会及时的取走终止状态,销毁僵尸进程.
- 父进程调用
wait()
和waitpid()
是非常重要的,因为只有这样才能获得子进程的终止状态.- 子进程暂停时,这两个函数也会返回相应的状态.
- "终止状态值"和"退出状态值"是不同的,后者是指进程正常终止时通过
return
或exit()
返回的指,退出状态值用不同的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一般会提供系统调用刷新缓冲.,unix中是fsync,sync和fdadasync.
- 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资源,效率更高.
readv
与writev
- readv:连续读取一个fd,把其中的值顺序存储到多个buf中
- writev:顺序从多个buf中读值,再写入fd中
- 这里面的多个buf形成了一个指针数组,也就是
vector
- 批量查询功能可以一次查询多个fd的状态,从而判断其是否可读/可写/异常. 相比于手动轮询每个fd的状态, 这组API在等待期间可以让出CPU资源,效率更高.
进程间通信
为了可扩展性, 原则上进程间通信最好仅使用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
,其编程模型更为简单
- 如果是在本机进程之间使用socket,那么可以使用
- type: 传输方式,一般是DGRAM,采用带有目标地址的定长数据包; 或 STREAM,流式数据传输
- protocl: 使用0即可,采用与
domain+type
匹配的默认协议
UNIX中,将行为模式
type
和protocol
解除了绑定,这是为了保留弹性,例如以后除了一个新的FAST_TCP,它的行为和TCP一致,但是速度更快,此时的行为模式是可以不变的,但是协议可以直接换掉. 在目前,一般情况下,可以认为DGRAM就是UDP, STREAM就是TCP -
socket的典型工作模式为: host1_ <-> host1_sockfd <—->host2_sockfd <-> host2.
- socket传输数据一定需要一对
socket_fd
,从一个fd写入的只能由另一个fd读出.
- socket传输数据一定需要一对
-
socketfd默认是空的, 需要通过
bind
来将其绑定到具体的socket file (如果对应的socket file不存在,bind 会创建一个新的), 具体而言,socket address
和socket 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_addr
和ret_info->ai_next
是最常用的两个成员,前者是sockaddr *
型的,可以给用户使用,后者是链表的next.- 输入的
hint
相当于额外的输入参数,主要用于过滤返回的ret_info
- 输入的
- host和service形式很多,典型的如
为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,可以用
getsockname
和getpeername
来查找自身以及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];