第一章 TCP/IP协议族
1.1 TCP/IP 体系结构以及主要协议
数据链路层(ARP、Data Link RARP)、网络层(ICMP IP)、传输层(TCP UDP SCTP)、应用层(ping telnet OSPF DNS)四层协议组成TCP/IP协议族。
1.1.1 数据链路层
数据链路层实现了网卡几口的网络驱动程序,不同网卡设备有不同的驱动,数据链路层抽象了数据在物理媒介的传输。提供给上层统一的接口。
ARP协议(Address Resolve Protocal, 地址解析协议)、RARP(Reverse Address Resolve Protocal, 逆地址解析协议) 实现IP地址和物理地址(通常是MAC地址)相互转换。ARP协议负责 IP => MAC映射、RARP协议负责MAC => IP转换。
1.1.2 网络层
网络层实现数据包的选路和转发,即负责数据包从那条链路发送数据包。
IP协议(Internet Protocol, 因特网协议),根据数据包中的IP地址投递到目标主机。
ICMP协议(Internet Control Message Protocol, 因特网控制报文协议),用以检测网络连接,报文格式由8位类型+8位代码+16位校验和+不定长报文内容组成。报文类型:
- 差错控制报文: 回应网络错误,类型3-网络不可达,类型5-重定向(代码0 - 网络重定向,1-主机重定向)
- 查询报文:查询网络信息,类型8-目标是否可达
校验和: 对整个报文(头部和内容)进行循环冗余校验(Cyclic Redundancy Check, CRC)。ICMP协议标准文档RFC 792。
1.1.3 传输层
为主机程序提供端到端(end to end)的通讯。传输层为应用程序封装了一条端到端的逻辑通讯链路,负责数据的收发、链路的超时重连。网络层封装网络连接的细节,数据链路层(驱动)封装物理网络的电气细节。
TCP协议(Transmission Control Protocol, 传输控制协议),提供可靠的、面向连接和基于流(stream)的服务。使用超时重传、数据确认保证数据包被正确地发送到目的端,没有传输长度限制。
UDP协议(User Datagram Protocol, 用户数据报协议),不可靠、无连接基于数据报的服务,无法保证数据正确传到目的端,目的端通过校验发现数据错误则丢弃,并通知程序数据发送失败。
SCTP协议(Stream Control Transmission Protocal, 流控制传输协议)用来在Internet传输网络信号设计的。
1.1.4 应用层
应用层负责处理应用程序的逻辑,用户态中实现的。
ping 利用ICMP报文检测网络连接
telnet协议:远程登录协议
OSPF(Open Shortest Path First, 开放最短路径优先)协议是一种动态路由更新协议,路由器之间的通信,告知对方各自的路由信息。
DNS(Domain Name Service, 域名解析服务)协议域名到IP地址的转换。
1.2 封装
本层协议通过将在上层数据的基础上加上自己的头部信息(有时候包含尾部信息),以实现该层的功能,该过程称为封装。从应用层到链路层,层层添加数据。
TCP封装后的数据称为TCP报文,UDP封装后称为UDP数据包,IP数据包,数据链路层封装为帧(frame)。
帧的最大传输单元(Max Transmit Unit, MTU), 即帧最多能携带多少上层协议数据,受网络类型限制,以太网帧的MTU是1500字节。
1.3 分用
当帧数据到达目的主机时,从链路层到应用层,层层解析数据。这个过程称为分用(demultiplexing)。对于上层来说,下层的分用是透明的。不需要考虑下层的处理流程,只要该层发送的数据能够正确的发送到目的端的对应的层就行。
以太网帧类型值0x800说明帧数据为IP数据报。
以太网帧类型值0x806说明帧数据为ARP请求或应答报文。
第2章 IP协议详解
第5章 Linux网络编程基础API
socket地址API: socket包含一个IP地址和端口对(ip,port),唯一表示了TCP通信的一端。
socket基础API: 位于sys/sockect.h
头文件中,包括创建socket,命名socket,监听socket,接受连接,发起连接,读写数据,获取地址信息,检测外带标记,读取和设置socket选项。
网络信息API: Linux提供一套网络信息API,主机名和IP地址之间的转换,服务名称和端口号之间的转换。在netdb.h
。
5.1 socket地址API
5.1.1 主机字节序和网络字节序
大端序(big endian)和小端序(little endian)。大端序整数高位字节(23~31bit)存储在内存的低地址,低位字节放在高位地址。小端序,高位放高位,低位放低位。
整数0xffee
在内存块中存储如下为大端序。ff为整数大端,放在了内存的低位。
1 | 0+++++++16+++++31 |
判断大小端,只要看内存低位存储的是高位数据和低位数据。检查机器字节序代码。
1 | void byteorder(){ |
通常PC都是采用小端序,也称为主机字节序。
发送端将数据转换成大端字节序发送,接收端根据自身使用的字节序对数据进行处理。大端序也称为网络序。相同机器的两个进程通信也要考虑节序问题。(JAVA虚拟机采用大端序)
linux常用的主机字节序和网络字节序之间的转换。
1 |
|
5.1.2 通用socket地址
1 |
|
sa_family成员是地址族类型(sa_family_t)的变量,地址族类型通常和协议族对应,协议族(protocol family,也称为domain)和地址族对应
协议族 | 地址族 | 描述 |
---|---|---|
PF_UNIX | AF_UNIX | UNIX本地域协议族 |
PF_INET | AF_INET | TCP/IPv4协议族 |
PF_INET6 | AF_INET6 | TCP/IPv6协议族 |
PF_*
和AF_*
定义bits/socket.h
文件中,前后者有完全相同的值,两者通常混用。
sa_data用来存放socket地址值,不同协议族有不同的地址。
协议族 | 地址值含义和长度 |
---|---|
PF_UNIX | 文件的路径名,长度可达108字节 |
PF_INET | 16bit端口号和32bitIPv4地址,共16字节 |
PF_INET6 | 16bit端口号,32bit流标识,128bitIPV6地址,32bit范围id,共26字节 |
14字节的sa_data不能满足需求,所以定义下面的通用结构体。
1 |
|
5.1.3 专用socket地址
UNIX本地域协议族使用下面的结构体
1 |
|
TCP/IP协议族sockaddr_in和sockaddr_in6两个专用socket地址结构体,它们分别用于IPv4和IPv6。
IPv4
1 | struct sockaddr_in |
IPv6
1 | struct sockaddr_in6 |
5.1.4 IP地址转换函数
字符串的ip地址转成数字,数字转成ip地址。
1 |
|
1 | /*af常用宏*/ |
inet_ntoa
函数不可重入测试代码
1 | void inet_ntoa_test() { |
输出:
1 | address[0x7f531625e4d0] 1 : 184.251.2.0 |
可以看得出来,返回的字符串指向的地址是一样的,调用之后需要复制生成的值。
5.2 创建socket
socket是可读、可写、可控制、可关闭的文件描述符。
1 |
|
- domain参数告诉系统使用协议族TCP/IP协议族使用PF_INET(ipv4)、PF_INET6(ipv6)。本地协议族PF_UNIX.
- type参数指定服务类型。SOCK_STREAM流服务,SOCK_DGRAM数据报服务。TCP/IP协议族而言SOCK_STREAM表示使用TCP协议,SOCK_DGRAM表示使用UDP协议。非阻塞socket类型SOCK_NONBLOCK,fork调用创建子进程中关闭该socket的类型SOCK_CLOEXEC。
- protocol协议表示在前两个参数构成的协议集合下,再进一步确定具体的协议。一般是设为0.
socket系统调用成功返回socket文件描述符,失败返回-1,并设置errno。
5.3 命名socket
创建了socket指定了地址族,但是并没有指定地址族中的具体socket地址。创建了一个TCP的socket,但是并没有指定这个socket的具体地址。将一个socket与socket地址绑定(bind)称为给socket命名。作为服务端的话需要绑定一个固定的地址,方便其他人连。作为客户端可以不绑定socket,使用自动分配的socket去连接服务器。
1 |
|
bind函数将my_addr所对应的地址绑定到sockfd描述符中,addrlen该socket地址的长度。
绑定成功返回0,失败返回-1,并设置errno.常见的errno,EACCES(地址是受保护的地址,普通用户绑定到0~1024端口中)和EADDRINUSE(被绑定的地址正在使用中).
5.4 监听socket
socket绑定之后需要开启监听队列才能处理客户连接。
1 |
|
sockfd被监听的socket, backlog内核监听队列的最大长度,监听队列长度超过backlog,服务器不在受理新的客户连接,客户端收到ECONNREFUSED错误信息。2.2的内核版本之前的linux,backlog参数表示处于半连接(SYN_REVD)和完全连接状态(ESTABLISHED)的socket的上限。2.2内核版本之后,只表示处于完全连接状态的socket的上限,处于半连接状态的socket上限由/proc/sys/net/ipv4/tcp_max_syn_backlog
内核参数定义。典型的值为5。完整的连接最多有(backlog + 1)个。
5.5 接受连接
从listen监听队列中接受一个连接
1 |
|
sockfd执行listen系统调用的监听的socket。addr参数接收连接端的socket地址,长度由addrlen参数决定。accept成功时返回一个新的连接socket,这个socket唯一的标识了被接受的这个连接。这个socket和监听的socket不一样,可以读写该socket与客户端通信。accept失败返回-1并设置errno。测试的代码
1 |
|
测试结果:
1 | ./accept_test 127.0.0.1 12345 |
5.6 发起连接
客户端通过系统调用来主动连接服务器
1 |
|
sockfd由socket系统调用返回的一个socket,serv_addr参数是服务器监听的socket地址,addrlen指定这个地址长度。connect成功返回0, 建立成功后sockfd唯一的标识这个链接。建立连接错误返回-1并设置errno。常见的errno是ECONNERFUSED目标端口不存在,连接被拒绝。ETIMEDOUT连接超时。
5.7 连接关闭
关闭连接对应的socket,可以使用普通文件描述符的系统调用完成。
1 |
|
fd为待关闭的socket,close是将fd的引用技术减1。当fd的引用计数为0时,才真正的关闭连接。在多进程程序中,一次fork系统调用默认将使父进程中打开的socket+1,我们必须在父进程和子进程中都对该socket执行close调用才能将连接关闭。无论如何都要立即终止连接,可以使用shutdown系统调用。
1 |
|
sockfd为待关闭的socket,howto决定怎么关闭。
可选项 | 含义 |
---|---|
SHUT_RD | 关闭sockfd的读一半,程序不能再对这个sockdf文件描述符进行读操作,并且该socket接收缓冲区中的数据都被丢弃。 |
SHUT_WR | 关闭sockfd的写一半,socket的发送缓冲区数据会在真正关闭连接之前全部发送出去,应用程序不能再对该连接进行写操作,连接处于半关闭状态。 |
SHUT_RDWR | 同时关闭读写 |
shutdown成功时返回0,失败则返回-1,并设置errno。
5.8 数据读写
对文件的读写操作read和write同样适用于socket。还提供了专门的函数。
TCP流数据读写系统调用
1 |
|
recv读取sockfd上的数据,buf和len参数分别指定读缓冲区的位置和大小,flag通常设置为0,recv成功时返回实际读取到的数据的长度,可能小于期望的长度,需要多次调用recv才能读取到完整数据。recv返回0说明对方关闭连接,出错返回-1并设置errno。
选项 | 含义 | send | recv |
---|---|---|---|
MSG_CONFIRM | 数据链路层协议持续监听对方的回应,直到得到答复。只能用于SOCK_DGRAM和SOCK_RAW类型的socket | Y | N |
MSG_DONTROUTE | 不查看路由表,直接将数据发送给本地局域网络内的主机。表示发送者确切的知道目标 主机就在本地网络上。 | Y | N |
MSG_DONTWAIT | 对socket的此次操作是非阻塞的。 | Y | Y |
MSG_MORE | 告诉内核应用程序还有更多数据要发送,内核将超时等待新的数据写入TCP发送缓冲区后一并发送,这样可防止TCP发送过小的报文段,从而提高传输效率。 | Y | N |
MSG_WAITALL | 读操作仅在读取到指定数量的字节后才返回 | N | Y |
MSG_PEEK | 窥探读缓冲中的数据,此次操作不会导致这些数据被清除。 | N | Y |
MSG_OOB | 发送或接收紧急数据 | Y | Y |
MSG_NOSIGNAL | 往读端关闭的管道或socket连接中写数据时不引发SIGPIPE信号 | Y | N |
使用MSG_OOB发送和接收数据
接收数据
1 |
|
发送数据
1 |
|
5.8.2 UDP数据读写
函数调用
1 |
|
flags的值含义和TCP的flags的值一致。返回值含义和TCP的一致。recvfrom/sendto也可以使用Stream的socket数据读写,需要把最后两个参数设为NULL就行。
5.8.3 通用数据读写
可以用于TCP和UDP数据的读写
1 |
|
msghdr定义
1 | struct msghdr |
msg_name成员指向一个socket地址结构变量,指定通讯对方的地址,TCP连接没有意义,设置为NULL。
msg_iov结构体指针
1 | struct iovec |
msg_iovlen指定这样的msg_iov对象有多少个。对于recvmsg,读取到的数据放在msg_iovlen块分散的内存中,内存的位置和长度则由msg_iov指向的数组指定,称为分散读(scatter read)。对于sendmsg,msg_iovlen块分散的内存将被一并发送,称为集中写(gather write);
msg_control和msg_controllen用于辅助数据的传送。
msg_flags不用指定,会自己复制recvmsg/sendmsg的flag参数的内容。recvmsg和sendmsg的返回值和send/recv的flags参数相同。
5.9 外带标记
1 |
|
sockatmark判断sockfd是否处于带外标记,即下一个读取到的数据是否是带外数据。是返回1,使用MSG_OOB标志接收recv带外数据。不是返回0;
5.10 地址信息函数
1 |
|
getsockname获得sockfd对应的本端的socket地址,socket的长度存储于address_len中。socket地址大于address的大小,socket地址被截断。成功返回0,失败返回-1,并设置errno;
getpeername获得sockfd对应的远端socket地址,含义和getsockname一致。
5.11 socket选项
设置和读取socket文件描述符属性的方法
1 |
|
sockfd指定被操作的目标socket。level指定要操作哪个协议选项IPv4、IPv6、TCP等,option_name指定选项名称,option_value选项的值,option_len选项长度。
level: SOL_SOCKET(通用socket选项,与协议无关)
option_name | 数据类型 | 说明 |
---|---|---|
SO_DEBUG | int | 打开调试信息 |
SO_REUSEADDR | int | 重用本地址 |
SO_TYPE | int | 获取socket类型 |
SO_ERROR | int | 获取并清楚socket错误状态 |
SO_DONTROUTE | int | 不查看路由器表,直接将数据发送给本地局域网内的主机。含义和send系统调用的MSG_DONTROUTE标志类似。 |
SO_RCVBUF | int | TCP接收缓冲区大小 |
SO_SNDBUF | int | TCP发送缓冲区大小 |
SO_KEEPALIVE | int | 发送周期性报文以维持连接 |
SO_OOBINLINE | int | 接收到的带外数据将存留在普通数据的输入队列中(在线存留),此时我们不能使用带MSG_OOB标志来读取带外数据(需要像读取普通数据那样读取带外数据) |
SO_LINGER | linger | 若有数据待发送,则延迟关闭 |
SO_RCVLOWAT | int | TCP接收缓存区低水位标记 |
SO_SNDLOWAT | int | TCP发送缓冲区低水位标记 |
SO_RCVTIMEO | timeval | 接收数据超时 |
SO_SNDTIMEO | timeval | 发送数据超时 |
level:IPPROTO_IP(IPv4选项)
option_name | 数据类型 | 说明 |
---|---|---|
IP_TOS | int | 服务类型 |
IP_TTL | int | 存活时间 |
level:IPPROTO_IPV6(IPv6选项)
option_name | 数据类型 | 说明 |
---|---|---|
IPV6_NEXTHOP | sockaddr_in6 | 下一跳的地址 |
IPV6_RECVPKTINFO | int | 接收分组信息 |
IPV6_DONTFRAG | int | 禁止分片 |
IPV6_RECVTCLASS | int | 接收通讯类型 |
level:IPPROTO_TCP(TCP选项)
option_name | 数据类型 | 说明 |
---|---|---|
TCP_MAXSEG | int | TCP最大报文段大小 |
TCP_NODELAY | int | 禁止Nagle算法 |
部分的socket选项在调用listen系统调用前设置才有效,有些socket选项却应该在TCP同步报文段设置。解决方案:对监听socket设置这些socket选项,那么accept返回的连接socket将自动继承这些选项(SO_DEBUG、SO_DONTROUTE、SO_KEEPALIVE、SO_LINGER、SO_OOBINLINE、SO_RCVBUF、SO_RCVLOWAT、SO_SND_BUF、SO_SNDLOWAT、TCP_MAXSEG、TCP_NODELAY),客户端,这些socket选项应该在调用connect函数之前设置。
5.11.1 SO_REUSEADDR选项
通过设置该选项,socket可以强制使用被处于TIME_WAIT状态的连接占用的socket地址。也可以通过修改内核参数/proc/sys/net/ipv4/tcp_tw_recycle
来快速回收被关闭的socket,使得TCP根本不进入TIME_WAIT状态,允许应用程序立即重用本地的socket地址。
5.11.2 SO_RCVBUF和SO_SNDBUF选项
TCP接收缓冲区和发送缓冲区的大小。设置大小时,将值加倍,并且不得小于某个值。TCP接收缓冲区最小值是256字节,发送区最小值是2048字节。这样做的目的就是确保一个TCP连接拥有足够的空闲缓冲区来处理拥塞。也可以修改内核参数/proc/sys/net/ipv4/tcp_rmem
和/proc/sys/net/ipv4/tcp_wmem
强制TCP接收和发送缓冲区没有大小限制。
5.11.3 SO_RCVLOWAT和SO_SNDLOWAT选项
一般被I/O复用系统调用来判断socket是否可读或可写。当TCP接收缓冲区可读数据的总数大于标记水位,I/O复用系统调用将通知应用程序可以从对应的socket上读取数据。当TCP发送缓冲区中的空闲大于其低水位标记时,I/O复用系统将通知应用程序可以往对应的socket上写入数据。
5.11.4 SO_LINGER选项
SO_LINGER选项用于控制close系统调用关闭TCP连接时的行为。默认情况下,使用close关闭socket时,close将立即返回,将该socket发送缓冲区残留数据发送给对方。linger类型
1 |
|
- l_onoff等于0。此时SO_LINGER不起作用,close用默认行为关闭socket。
- l_onoff不为0。l_linger等于0。此时close系统调用立即返回,TCP模块将丢弃被关闭的socket发送缓冲区残留的数据,同时发送一个复位报文段。可以用来处理异常终止一个连接的方法。
- l_onoff不为0,l_linger大于0。阻塞的socket,close等待l_linger的时间,等待发送所有的残留数据并得到对方的确认。超时返回-1,并设置errno为EWOULDBLOCK。非阻塞socket,close立即返回,根据errno判断残留数据是否已经发送完毕。
5.12 网络信息API
5.12.1 gethostbyname和gethostbyaddr
1 |
|
gethostbyname根据域名获得主机完整信息,gethostbyaddr函数根据IP获得主机完整信息。type参数包括AF_INET和AF_NET6
5.12.2 getservbyname和getservbyport
1 |
|
根据名称或根据端口获得服务,主要是读取/ect/services文件夹获得服务信息。
5.12.3 getaddrinfo
1 |
|
getaddrinfo即能通过主机名获得IP地址,也可以通过服务名获得端口号。是否可重入取决于内部调用的gethostbyname和getservbyname函数是否是可重入版本。
ai_protocol指具体协议,与socket系统调用第三个参数一致,通常设置为0。
ai_flags如下表按位或
选项 | 含义 |
---|---|
AI_PASSIVE | 在hints参数中设置,表示调用者是否会取得的socket地址用于被动打开。服务器通常需要设置它,表示接受任何本地socket地址上的服务请求,客户端不能设置它。 |
AI_CANONNAME | 在hints参数中设置,告诉getaddrinfo函数返回主机的别名。 |
AI_NUMBERICHOST | 在hints参数中设置,表示hostname必须是用字符串表示的IP地址从而避免了DNS查询。 |
AI_NUMBERICSERV | 在hints参数中设置。强制service参数使用十进制端口号字符串形式,而不能是服务名。 |
AI_V4MAPPED | 在hints参数中设置。如果ai_family被设置为AF_INET6,那么当没有满足条件的IPv6的地址被找到时,将IPv4地址映射为IPv6地址。 |
AI_ALL | 必须和AI_V4MAPPED同时使用,否则将被忽略。表示同时返回符合条件的IPv6地址以及IPv4地址映射得到的IPv6地址。 |
AI_ADDRCONFIG | 仅当至少配置有一个IPv4地址(除回路地址)时,才返回IPv4地址信息。仅当至少配置有一个IPv6地址(除回路地址)时,才返回IPv6地址信息。和AI_V4MAPPED是互斥的。 |
使用hints参数的时候,可以设置ai_flags,ai_family,ai_socktype和ai_protocol四个字段,其他字段则必须被设置为NULL。
5.12.4 getnameinfo
getnameinfo函数能通过socket地址同时获得以字符串表示的主机名和服务名,是否可重入取决于内部使用的gethostbyaddr和getservbyport函数是否可重入版本。
1 |
|
getnameinfo返回主机名存储host参数指向的缓存中,服务名存储在serv参数指向的缓存中,hostlen和servlen参数指定这两块缓存长度。flags参数控制getnameinfo行为。
flags参数
参数 | 含义 |
---|---|
NI_NAMERQD | 如果通过socket地址不能获得主机名,则返回一个错误。 |
NI_DGRAM | 返回数据报服务。大部分同时支持流和数据报服务使用相同端口号来提供。端口512-514例外,TCP的514提供的是shell登录服务。 |
NI_NUMBERICHOST | 返回字符串表示的IP地址而不是主机名 |
NI_NUMBERICSERV | 返回字符串表示的十进制端口号而不是服务名 |
NI_NOFQDN | 仅返回主机域名的第一部分,nebula.testing.com只存储nebula写入缓存中。 |
getaddrinfo和getnameinfo函数成功时返回0,失败返回错误码。
选项 | 含义 |
---|---|
EAI_AGAIN | 调用临时失败,提示应用程序过后再试 |
EAI_BADFLAGS | 非法ai_flags值 |
EAI_FAIL | 名称解析失败 |
EAI_FAMILY | 不支持ai_family参数 |
EAI_MEMORY | 内存分配失败 |
EAI_NONAME | 非法的主机名或服务名 |
EAI_OVERFLOW | 用户提供的缓冲区溢出,仅发生在getnameinfo调用中 |
EAI_SERVICE | 没有支持的服务。使用数据报来寻找ssh服务,ssh只能使用流服务。 |
EAI_SOCKTYPE | 不支持的服务类型。hints的ai_socktype和ai_protocol不一致,第一个指定SOCK_DGRAM,后者使用IPROTO_TCP会导致该错误。 |
EAI_SYSTEM | 系统错误,错误值在errno中 |
错误码转成字符串形式
1 |
|
第6章 高级I/O函数
- 用于创建文件描述符的函数,包括pipe、dup/dup2函数。
- 用于读写数据的函数,包括readv/writev、sendfile、mmap/munmap、splice和tee函数。
- 用于控制I/O行为和属性的函数,包括fcntl函数。
6.1 pipe函数
1 |
|
fd[0]
和fd[1]
分别构成管道两端,fd[0]
只能用于从管道读出数据,fd[1]
只能用于往管道写入数据。数据流fd[1]-->fd[0]
,管道都是阻塞的,读空阻塞,写满阻塞。想要实现双向通讯需要两个管道。写端关闭(引用计数为0),读端read反0(EOF)。读端关闭,写端write失败,引发SIGPIP信号。传输字节流,Linux 2.6.11之后默认65536字节,fcntl可修改容量。
创建双向管道的函数
1 |
|
前3个入参socket函数含义一致。domain使用AF_UNIX
,只能本地使用双向管道。fd[2]参数可读可写。
6.2 dup函数和dup2函数
把标准输入重定向到一个文件或标准输出重定向到一个网络连接。使用dup或dup2函数完成。
1 |
|
例子:
1 |
|
readv函数和writev函数
readv函数是将数据从文件描述符读到分散的内存块中,分散读。writev函数则将多块分散内存数据一并写入文件描述符中,集中写。
1 |
|
例子:
demo6_3
1 |
|
6.4 sendfile函数
sendfile函数处于内核中两个文件描述符之间直接传递数据,没有内核缓冲区和用户缓冲区之间数据拷贝,零拷贝,效率高。
1 |
|
in_fd待读出内容的文件描述符,out_fd写入内容的文件描述符。offset参数指定从读入文件流的那个位置开始读,为空默认起始位置。count为传输字节数。sendfile成功返回字节数,失败-1设置errno。要求:in_fd必须支持mmap函数,必须指向真实文件,不能是socket。out_fd必须是一个socket。
demo6.4 文件传输
1 |
|
和demo6_3相比,不需要自己申请空间,实现了文件的传送,效率更高。
6.5 mmap函数和munmap函数
mmap
函数申请一段内存空间,可作为进程间通讯的共享内存,也可以将文件直接映射到里面。munmap
函数释放mmap
申请的内存空间。
1 |
|
start
参数允许用户使用某个特定地址段作为这段内存的起始地址。设置为NULL由系统自动分配。length
设定内存段长度。prot
设置内存段的访问权限,以下可按位或。
序号 | 值 | 说明 |
---|---|---|
1 | PROT_READ | 内存段可读 |
2 | PROT_WRITE | 内存段可写 |
3 | PROT_EXEC | 内存段可执行 |
4 | PROT_NONE | 内存段不能被访问 |
flags
控制内存段内容被修改后程序行为,常用的值如下。可按位或。MAP_SHARED
和MAP_PRIVATE
互斥。
序号 | 值 | 说明 |
---|---|---|
1 | MAP_SHARED | 在进程间共享这段内存,对该段内存的修改将被反映到文件中。提供进程间共享内存的POSIX方法 |
2 | MAP_PRIVATE | 内存段为调用进程私有,对该段内存段的修改不会反映到被映射的文件中。 |
3 | MAP_ANONYMOUS | 这段内存不是从文件映射而来的,内容初始化为0,mmap后两个参数被忽略。 |
4 | MAP_FIXED | 内存段必须位于start参数指定的地址处,start必须是内存页面大小(4096)的整数倍 |
5 | MAP_HUGETLB | 按照”大内存页面”来分配内存空间。“大内存页面”的大小可通过/proc/meminfo 文件查看 |
fd
被映射文件对应的描述符,一般open获得。offset参数设置从文件的何处开始映射。
mmap
函数成功返回目标内存区域的指针,失败返回MAP_FAILED((void*)-1
,并设置errno。munmap
函数成功时返回0,失败返回-1设置errno。
6.6 splice 函数
用于两个文件描述符之间移动数据,零拷贝。
1 |
|
fd_in
参数是待输入数据的文件描述符。如果是管道文件描述符,off_in
参数必须设置为NULL。fd_in
不是管道文件描述符(socket)等,off_in
表示从输入数据流的何处开始读取数据。off_in
为NULL,表示从输入数据流的当前偏移位置读入。off_in
不为NULL,具体偏移位置。fd_out/off_out
与上述输入一致。len
控制数据移动长度。flags
参数控制数据如何移动,可以按位或。
序号 | 值 | 含义 |
---|---|---|
1 | SPLICE_F_MOVE | 如果合适的话,按整页内存移动数据。由于有bug。从内核2.6.21后,该flags没有任何效果 |
2 | SPLICE_F_NONBLOCK | 非阻塞的splice操作,但实际效果还是受文件描述符本身的阻塞状态影响。 |
3 | SPLICE_F_MORE | 给内核一个提示:后续的splice调用将读取更多的数据。 |
4 | SPLICE_F_GIFT | 对splice没有效果 |
splice函数,fd_in
和fd_out
至少有一个是管道文件描述符。splice函数调用成功时返回移动字节的数量。可能返回0,没有数据需要移动,这发生从管道中读取数据(fd_in是管道文件描述符)而管道没有被写入任何数据时。splice函数返回-1并设置errno。常见errno表。
序号 | 值 | 说明 |
---|---|---|
1 | EDADF | 参数所指的文件描述符有错 |
2 | EINVAL | 目标文件系统不支持splice,或目标文件以追加方式打开,或两个文件描述符都不是管道文件描述符,或者某个offset参数被用于不支持随机访问的设备 |
3 | ENOMEM | 内存不够 |
4 | ESPIPE | 参数fd_in或fd_out是管道文件描述符,而off_in或off_out不为NULL。 |
零拷贝回射服务器
1 |
|
6.7 tee函数
tee函数在管道文件描述符之间复制数据,零拷贝操作。不消耗数据,因此源文件描述符上的数据仍然可以可以后续读操作,tee函数的原型。
1 |
|
参数与splice, fd_in
和fd_out
都是管道描述符。成功复制时返回两个文件描述符之间复制的数据量(字节数)。返回0表示没有复制任何数据,失败返回-1设置errno。
实现tee程序
实现tee程序,同时将数据输出到控制台和文件。
1 |
|
fcntl函数
file control
, 提供对文件描述符各种控制操作。还有ioctl
比fcntl
能够执行更多的控制。fcntl
为POSIX规范指定的方法。
1 |
|
fd
被操作的文件描述符,cmd参数指定执行何种类型的操作。
- 复制文件描述符
操作 | 含义 | 第三个参数类型 | 成功时返回值 |
---|---|---|---|
F_DUPFD | 创建一个新的文件描述符,其值大于或等于arg | long | 新创建的文件描述符的值 |
F_DUPFD_CLOEXEC | 与F_DUPFD类似,不过创建文件描述符的同时,设置其close-on-exec标识 | long | 新创建的文件描述符的值 |
- 获取和设置文件描述符的标识
操作 | 含义 | 第三个参数的类型 | 成功时返回值 |
---|---|---|---|
F_GETFD | 获得fd标志,比如close-on-exec标志 | 无 | fd的标志 |
F_SETFD | 设置fd的标志。 | long | 0 |
- 获取和设置文件描述符的状态
操作 | 含义 | 第三个参数类型 | 成功返回值 |
---|---|---|---|
F_GETFL | 获取fd的状态标志,可以由open系统调用设置的标志(O_APPEND、O_CREAT),访问模式(O_RDONLY、O_WRONLY和O_RDWR) | void | fd的状态标志 |
F_SETFL | 设置fd的状态标志,但部分标志是不可能被修改 | long | 0 |
- 管理信号
操作 | 含义 | 第三个参数类型 | 成功返回值 |
---|---|---|---|
F_GETOWN | 获得SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID | 无 | 信号的宿主进程的PID或进程组的组ID |
F_SETOWN | 设置SIGIO和SIGURG信号的宿主进程的PID或进程组的组ID | long | 0 |
F_GETSIG | 获取当应用程序被通知fd可读或可写时,是哪个信号通知该事件 | 无 | 信号值,0表示SIGIO |
F_SETSIG | 设置当fd可读或可写时,系统应该触发哪个信号来通知应用程序 | long | 0 |
- 操作管道容量
操作 | 含义 | 第三个参数的类型 | 成功时的返回值 |
---|---|---|---|
F_SETPIPE_SZ | 设置由fd指定的管道的容量。/proc/sys/fs/pipe-size-max内核参数指定了fcntl能设置的管道容量的上限 | long | 0 |
F_GETPIPE_SZ | 获取由fd指定的管道的容量 | 无 | 管道容量 |
将文件描述符设置为非阻塞。
1 | int setnonblocking(int fd) |
第7章 linux服务器程序规范
- 一般为后台进程(守护进程daemon),父进程通常为init进程
- 含有日志系统,至少能够输出到日志文件
- 非root身份运行,有自己的运行账户
- 可配置的,多命令行或配置文件启动管理
- 生成pid文件到
/var/run
目录中,用以记录后台进程的PID - 考虑系统资源和限制,能够承受负荷(文件描述符、内存总量)
7.1 日志
7.1.1 Linux系统日志
syslogd
为旧版的系统使用守护进程处理系统日志,rsyslogd
为升级版的。rsyslogd
可以接收用户进程输出日志和内核输出日志。用户可以调用syslog
函数生成系统日志。日志输出到/dev/log
中。rsyslogd
监听该文件用来监听用户进程输出。内核日志之前使用rklogd
来管理,现在rsyslogd
额外模块实现了相同的功能。内核使用printk
等函数打印到内核环状缓存(ring buffer)中,环状缓存直接映射到/proc/kmsg
文件中。rsyslogd
读取该文件获得内核日志。rsyslogd
得到日志之后,输出到日志文件。调试信息/var/log/debug
文件,普通信息/var/log/messages
文件,内核消息/var/log/kern.log
。如何分配可在rsyslogd
配置文件中设置。/etc/rsyslog.conf
为rsyslogd
配置文件。
配置文件可以设置项:
- 内核日志输入路径
- 是否接收UDP日志及其监听端口(默认514,
/etc/service
文件) - 是否接收TCP日志及其监听端口
- 日志文件权限,包含哪些子配置文件
/etc/rsyslog.d/*.conf
rsyslogd
的子配置文件指定各类日志的目标存储文件
7.1.2 syslog函数
应用程序使用syslog
函数与rsyslogd
守护进程通讯。
1 |
|
变参,第二个参数message和第三个参数形成结构化输出,priority
设施值与日志级别按位或。设施值默认值是LOG_USER。
日志级别
1 |
openlog
函数可以改变syslog
的输出方式,进一步结构化日志内容
1 |
|
ident
参数指定的字符串被添加到日志消息的时间和日期之后,常常设置为程序的名字。logopt
参数影响后续的syslog
的调用行为,参数值可按位或。
1 |
facility
可以使用syslog
函数中设施默认值。
设置日志掩码可以对特定日志级别的数据进行过滤,日志级别大于日志掩码的都被忽略掉。返回设置之前的日志掩码。
1 |
|
使用closelog
函数可以关闭日志功能。
1 |
|
7.2 用户信息
7.2.1 UID、EUID、GID和EGID
大部分的服务器程序必须以root身份启动,但不能以root身份运行。下面函数可以获得和设置当前进程的真实用户ID(UID)、有效用户ID(EUID)、真实组ID(GID)和有效组ID(EGID):
1 |
|
一个进程拥有两个用户ID:UID和EUID。EUID存在的目的是方便资源的访问:允许运行程序的用户拥有该程序有效用户的权限。su程序的UID是所有人,有效用户的root。所有人可以用su来修改账户信息,修改账户信息需要访问/etc/passwd
,这个文件只有root
用户才有权限。那么普通用户就需要依靠一个中间人来访问该文件。普通用户可以依靠中间人的身份(EUID)访问中间人的资源。同理,真实组和有效组也是一样的道理。运行该程序的用户id对应uid,该程序的所有者对应euid.
- demo
1 |
|
编译之后执行以下命令
1 | sudo chown root:root test_uid # 修改目标文件所有者为root |
使用普通用户运行之后结果为
1 | userid is : 1000, effective userid is : 0 |
7.2.2 切换用户
以root身份启动的进程切换为以一个普通用户身份运行
1 | static bool switch_to_user(uid_t user_id, gid_t gp_id) { |
7.3 进程间关系
7.3.1 进程组
linux每个进程都有自己的PID,并且每个进程都有自己的进程组的ID(PGID)。
1 |
|
获得指定进程组的ID,失败返回-1设置errno。
每隔进程组都有一个首领进程,PID和PGID相同。进程组一直存在,直到所有进程都退出或加入其他进程组。
1 | #include<unistd.h> |
将pid对应的进程的进程组设置为pgid对应的进程组。pid和pgid相同,该进程设置为进程组首领。pid为0,设置当前进程的PGID为pgid。pgid为0,使用pid作为目标的PGID。设置成功返回0,失败返回-1并设置errno。
一个进程只能设置自己或子进程的PGID。并且,当子进程调用exec系列函数之后,不能再在父进程中对它设置PGID。子进程开启新的进程,新的进程所属的进程组ID为创建该进程的子进程。如果这时候父进程更改子进程的PGID会影响孙子进程相关ID。
7.3.2 会话
一些关联的进程组将形成一个会话(session)
1 |
|
该函数不能由进程组首领进程调用,会产生错误。非首领进程调用
- 创建新的会话
- 调用进程称为会话的首领(调用该函数的进程),该进程是该会话的唯一成员
- 新建一个进程组,PGID为调用进程的PID,并且该进程成为该组的首领进程
- 调用进程甩开终端
成功返回PGID,失败返回-1并设置errno。
1 |
|
获得该进程pid和sid。
7.3.3 用ps命令查看进程关系
ps -o pid,ppid,pgid,sid,comm | less
命令可以查看进程之间的关系。
7.4 系统资源限制
读写资源限制
1 |
|
1 | struct rlimit{ |
rlim_t为整数类型,描述资源级别。rlim_cur指定资源的软限制,rlim_max指定资源的硬限制。软限制是一个建议性的、最好不要超越的限制,超越的话,系统可能向进程发送信号以终止其运行。CPU时间超过,产生SIGXCPU信号,文件尺寸超过限制,产生SIGXFSZ信号。只有root用户才可以改变资源的硬限制。也可以通过配置文件来永久改变系统软硬件限制。
常见的资源限制类型
资源限制类型 | 含义 |
---|---|
RLIMIT_AS | 进程虚拟内存总量限制(单位字节),操过该限制将使得某些函数(mmap)产生ENOMEM错误 |
RLIMIT_CORE | 进程核心转储文件(core dump)的大小限制(单位是字节),其值为0表示不产生核心转储文件 |
RLIMIT_CPU | 进程CPU时间限制(单位是秒) |
RLIMIT_DATA | 进程数据段(初始化数据data段,未初始化数据bss段和堆)限制(单位为字节) |
RLIMIT_FSIZE | 文件大小限制(单位是字节),超过该限制将使得某些函数(write)产生EFBIG错误 |
RLIMIT_NOFILE | 文件描述符数量限制,超过该限制将使得某些函数(比如pipe)产生EMFILE错误 |
RLIMIT_NPROC | 用户能够创建的进程数限制,超过该限制将使得某些函数(fork)产生EAGAIN错误 |
RLIMIT_SIGPENDING | 用户能够挂起的信号数量限制 |
RLIMIT_STACK | 进程栈内存限制(单位是字节),超过该限制将引起SIGSEGV信号 |
setrlimit
和getrlimit
成功返回0,失败返回-1并设置errno。
7.5 改变工作目录和根目录
获得当前工作目录和改变进程工作目录的函数分别是
1 |
|
buf
为存储当前工作目录,size
指定路径长度。如果目录绝对路径(加上‘\0’)超过size,gecwd
返回NULL并设置errno为ERANGE。如果buf为NULL,size有效。getcwd
自动申请内存存储,并返回指针。失败返回NULL并设置errno。
chdir
函数的path参数指定要切换到的目标目录。成功时返回0,失败时返回-1并设置errno。
改变进程根目录的函数是chroot
1 |
|
chroot
并不改变进程的当前工作目录,调用chroot之后还要使用chdir("/")
来将工作目录切换至新的根目录。改变根目录之后可能无法访问/dev等目录了。默认根目录是”/“,更改根目录为”/tmp”之后。使用”/dev”路径访问的路径其实是”/temp/dev”路径。所以,只有特权进程才能改变根目录。
7.6 服务器程序后台优化
将服务器程序以守护进程方式运行
1 |
|
可以使用下面的函数完成功能
1 |
|
nochdir
参数用于指定是否改变工作目录,如果给它传递0,则工作目录将被设置为”/“,否则使用当前工作目录。noclose参数为0时,标准输入、标准输出、标准错误输出都被重定向到/dev/null
文件。否则使用原来的设备。成功反0,失败-1设置errno。
第8章 高性能服务器程序框架
服务器解构为如下三个主要模块:
- I/O处理单元。四种I/O模型和两种高效事件处理模式
- 逻辑单元。两种高效并发模式,高效的逻辑处理方式–有限状态机
- 存储单元。不讲
8.1 服务器模型
8.1.1 C/S模型
8.1.2 P2P模型
8.2 服务器编程框架
8.3 I/O模型
8.4
信号
发送信号
使用kill函数发送信号
1 |
|
pid取值定义
|范围|说明|
|—–|—–|
|pid>0| 发送信号给pid的进程|
|pid=0|发送信号给本进程组内的其他进程|
|pid=-1|发送信号给除init进程之外的所有进程,需要发送者拥有对目标进程发送的信号的权限|
|pid < -1|发送信号给组ID为-pid的进程组中的所有成员|
使用sig=0可以作为一种检测目标进程或进程组是否存在。但不可靠,1.进程PID回绕,2.检测方法不是原子操作。
函数成功反0,失败返回-1,设置errno
|errno|description|
|—–|—–|
|EINVAL|无效信号|
|EPERM|没有权限|
|ESRCH|目标进程或进程组不存在|
接收信号
信号接收函数的定义原型
1 |
|
用户可以自定义也可以使用系统函数SIG_IGN(忽略目标信号)/SIG_DEL(使用信号的默认处理方式: 结束进程Term, 忽略信号Ign,结束进程转储Core, 暂停进程Stop, 继续进程Cont).
1 |