导图社区 linux-网络编程
本课程将带你一步一步学会在 Linux操作系统下编程,使用系统底层提供给我们的各种接口和函数,井深入内核,体验系统底层的精妙之处。
编辑于2022-10-12 14:55:46linux
文件操作
VFS文件管理系统
通过树状结构来管理文件系统,树状结构的每一个节点都是“目录节点”
树状结构具有一个“根节点”
VFS通过超级块来了解一个具有文件系统 的所有需要信息
四大对象
超级块
描述整个文件系统的信息
每个具体的文件系统都有自己的超级块,是在文件系统安装时建立 的,卸载时自动删除,数据结构是super_block
所有超级块对象都以循环链表的形式连接在一起
索引结点
存放具体文件的一般信息
目录项
存放目录项与对应文件的链接信息
文件对象。在内核区
存放打开文件与进程之间交互的信息
iNode
索引结点
大小
128B/256B格式化时就给定
一般每1/2KB就设置一个iNode
Dentry
目录项
文件对象file
进程通过文件描述符来访问文件
每个进程用一个files_struct结构来记录文件描述符的使用情况
Linux用一个文件对象来保存打开文件的位置,这个对象称为打开的文件描述符
file结构只要保存了文件的位置,还把指向文件索引结点的指针也放入其中
file结构形成一个双链表,称为系统打开表
不带缓冲的文件IO
文件描述符:非负整数,负责找到内核的文件对象
0:stdin
1:stdout
2:stderr
主要create、open、read、write、lseek、close
这里的不带缓存是指直接通过文件描述符来操作对象! 内核区叫文件对象,用户区叫FILE结构体
IO
参考:https://zhuanlan.zhihu.com/p/405446579
同步和异步
同步是指用户线程发起IO请求后需要等待或者轮询内核IO操作,完成后才能继续执行。异步是指用户线程发起IO请求后仍继续执行,当内核IO操作完成后回通知用户线程,或者调用用户线程注册的回调函数。
阻塞和非阻塞
阻塞和非阻塞的概念描述的是用户线程调用内核IO操作的方式,阻塞时指IO操作需要彻底完成后才能返回用户空间,非阻塞时指IO操作被调用后立即返回给用户一个状态值,无需等待IO操作彻底完成。
I/O模型:阻塞、非阻塞、同步、异步
同步阻塞IO(Blocking IO):即传统IO模型
发起IO就阻塞,直到完成
同步非阻塞IO(Non-blocking IO):默认常见的socket都是阻塞的,非阻塞IO要求socket被设置成NONBLOCK
发起IO后返回,但因为是同步的,则需要一直轮询IO是否完成
异步阻塞
异步非阻塞
IO多路复用
进程模型:单进程、多进程、多线程。
IO多路复用
目的
IO多路复用设计目的其实不是为了快,而是为了解决线程/进程数量过多对服务器开销造成的压力
不一定比多线程+io阻塞快
模型
select函数
select是内核提供的多路分离函数,使用它可以避免同步非阻塞IO中轮询等待问题。
一个进程可以注册多个socket,进而可以处理多个io请求
根据文件描述符来判断是否就绪
步骤
创建监听集合
初始化集合
把监听的FD放入到集合中
fd是文件描述符File descriptor
调用select函数使本进程陷入阻塞
监听集合中有一个fd就绪,就解除阻塞
使用FD_ISSET可以用来确认fd是否就绪,找到就绪的fd,执行IO操作
epoll
select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。而epoll其实也需要调用epoll_wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间。
进程
一些基础概念
并发:逻辑上同时 并行:同一时刻 单核CPU不能并行
每个进程都有一个系统分配的唯一标识符:进程ID(PID)
进程是整个操作系统的核心
从用户的角度来看,进程是一个程序的动态之执行过程
从操作系统的角度,进程是资源分配的最小单位
进程是并发执行的,即在一段时间内是同时执行的。但每个进程都以为自己在独享CPU,即虚拟CPU。时间片。 并行是指在同一时刻,
进程调度
进程是动态的
操作系统管理多个进程
进程组
进程的集合
一个进程只能属于一个进程组
组ID:第一个进程(组长)的PID。组长终止,组ID不变
父fork子 父子属于同一进程组
除了组长,都可以换组---setpgid()
setpgid():本进程脱离原组以自己为组长创建组。
通过shell启动的进程有自己单独的组
前台进程属于同一个!一个!组
子主题
进程调度
抢占式
时间片轮转
进程创建
bootleader-->0号进程
1
使用execve函数调用init进程,init进程创建了所有的应用进程
2
负责在内核中创建所有内核线程
父进程和子进程
进程的信息中包括它的进程ID和父进程ID
fork
用选择结构,以fork的返回值作为条件,区分父子进程
fork会返回两次,在父进程中返回子进程的pid,子进程中返回0
父子进程共享文件对象。对同一文件对象会共享读写位置,类似于dup
fork出来的子进程,继承了父进程的地址空间,包括上下文,进程堆栈,内存信息,打开的文件描述符,进程优先级等等。
子进程独有的只有其pid,资源使用和计时器
fork出来的子进程拥有父进程的完整副本,但不是一开始就有的,一开始父子进程共享内存,当子进程企图修改数据时,会触发写时复制,为子进程创建出该数据的一个副本。读操作不受影响!。子进程改变不会影响父进程。
孤儿进程
父进程先于子进程结束,子进程会变成孤儿进程,被1号进程接管
僵尸进程
子线程先退出,而父进程不调用wait或者waitpid来完成清理,则已经退出的子进程会成为僵尸进程。僵尸进程过多会影响系统进程。
僵尸进程不能转变成其他进程状态
wait和waitpid系统调用会阻塞父进程,等待一个退出的子进程并清理。
wait随机等待一个子进程,返回该子进程的pid
waitpid等待指定pid的子进程,-1表示等待所有子进程
守护进程
命名上以-D为结尾
后台服务程序,在后台运行,不受任何终端控制
脱离了启动该进程的会话
创建流程
父进程创建子进程,然后让父进程终止
在子进程中创建新对话
会话是一个或者多个进程组的集合
会话首进程(创建新会话的进程)的PID为该会话ID
会话的特征
一个会话可以有一个控制终端
终端是登录到Linux操作系统所需的入口设备,可以是远程的。
和控制终端建立连接的会话首进程称为控制进程(登录时自动连接,或者使用open打开文件/dev/tty
一个会话存在最多一个前台进程组和多个后台进程组,若会话与控制终端相连,则必定存在一个前台进程组
从终端输入的中断,会发给前台进程组的所有进程
终端断开连接,挂断信号会发给控制进程
会话是进程组的集合
修改当前工作目录为根目录,因为正在使用的目录是不能卸载的
重设文件权限码为0 ,避免创建文件的权限受限
关闭不需要使用的文件描述符,比如0,1,2
进程终止
3种正常方式
main函数中调用return
exit
_Exit或者_eixt函数
2种异常
调用abort函数
接收到引起进程终止的信号
进程间通信IPC
IPC为了打破隔离
管道
分类
有名管道
使用 mkfifo 可以创建管道文件, 使用 unlink 则可以删除所有文件包括管道文件
匿名管道
在一个进程中建立一个管道,再利用fork即可进行IPC
在文件系统中不存在,只能用于有亲缘关系的进程间
为了可移植性,采用半双工通信,两条管道即可全双工通信
int pipe(int pipefd[2])
使用pipe之前要先创建一个大小为2的整型数组,用于存放文件描述符 [0]是读端的fd,[1]是写端的
pipe[1] pipe[0] 内核管道缓冲区 pipe[1] pipe[0]
1->0
如果要实现父子进程之间全双工通信,需要调用 pipe 两次来创建两条管道。一个值得关注的是,fork 的次数不会影响管道的数量,每次使用 pipe 才会在内核创建一个管道缓冲区。
缺点:
使用时要频繁调用pipe系统调用
数据每次都要从写端拷贝到内核缓冲区,再拷贝到读端
共享内存
为了提高效率
ftok接口可以生成key,也可以手动设置
共享内存进程间通信步骤
int shmid = shmget(1000,4096,0600|IPC_CREAT);key为1000,大学为4096,创建一个0600的共享内存
char *p = (char*)shmat(shmid,null,0);//shmat接口,根据一个指定描述建立连接。第二个参数是在堆空间自动分配区域映射共享内存段,最后一个是权限,没啥用,0就行
往p指的地址填入数据即可。
读端重复12,读出数据即可
shmdt可以解除共享内存映射
拓展:看进程间通信笔记
多个进程同时写入,会造成竞态条件
信号量
互斥和同步
让多个进程共享同一片物理存储区域
描述可用共享资源数量本质是一个计数器,
P操作:if(sem>0) --sem;else等待。 测试并加锁,无资源则等待 条件判断和--操作是原子的
V操作:++sem; 释放并解锁
消息队列
面向消息的IPC
信号
一些基础概念
软件层面:异步的
进程间:同步?
为了实现进程的有序退出
信号递送表示内核执行信号处理流程
已经产生但未传递的信号称为挂起信号,也称未决信号
signal
注册信号
特点:
一次注册,回调永久生效
在递送x信号,会把x临时加入到mask
会自动重启低速系统调用
多个信号同时传送
信号执行中接收到另一个不同的信号
当前的中断,去执行新的
相同的
继续执行,执行完之后再执行新来的
连续重复相同类型的
后面的都被忽略,只多执行一次
mask:掩码 是位图
递送谁,就把谁加入到mask,递送完恢复
产生一个不在mask的信号,会立马递送
pending:未决信号集 也是位图
存储已产生但未递送的信号。只存一个。
一旦mask不阻塞某个信号,且pending中存在该信号,就取出并递送。
几个常见信号
kill 给别人发信号
raise 给自己发信号
线程
基本概念
资源分配的基本单位。线程是CPU调度的最小单位 注意!!!!进程是资源分配的最小单位!
why?
多进程吞吐能力大、编程简单,但是进程切换代价大。
进程切换
上下文切换:寄存器状态,PC,栈、堆
页表切换
改用线程
上线文切换:PC和栈
页表不用切换
进程都可以认为是一个单线程进程
每个线程在执行过程会在地址空间中有自己独立的栈。而堆、数据段、代码段、文件描述符和信号屏蔽字等资源则是共享的
目前使用最广泛的线程库名为NPTL
用户级线程和内核级线程
用户级
适用于IO密集型任务
内核级
适用于CPU密集型任务
混合型
应用程序会根据硬件中CPU的核心数创建若干个内核级线程,每一个内核级线程会对应多个有栈协程。这样在触发IO操作时,不再需要陷入内核态,直接在用户态切换线程即可。
errno是一个全局变量。多线程的错误处理不能用全局变量 用strerror处理报错信息
printf的原子性
两个线程同时执行printf,printf保证显示不会混杂,先后顺序不定
cout没有原子性
线程的创建
pthread_create()//线程id的地址、线程属性(默认null)、线程启动函数、主线程给子线程传递的参数
线程间传递参数
void* 可以看作是一个指针,解引用之前要强转
常把void* 当成一个long来传递
看做一个4B/8B的空间
让主、子线程指向同一片空间
栈区决定了能创建多少线程
在链接时需要加上 -lpthread
线程的主动退出
pthread_exit可以在任意位置退出线程
在线程入口函数执行return可以终止线程
捕获另一个线程的退出状态
线程的退出状态不一定非要创建他的线程来获取
主线程的退出状态获取不了
线程的退出
int pthread_join(pthread_t thread, void **retval);
调用 pthread_join 可以使本线程处于等待状态,直到指定的 thread 终止,就结束等待,并且捕获到的线程终止状态存入 retval 指针所指向的内存空间中。
因为线程的终止状态是一个 void * 类型的数据,所以 pthread_join 的调用者往往需要先申请8个字节大小的内存空间,然后将其首地址传入,在pthread_join 的执行之后,这里面的数据会被修改。如下: void *tret;//在调用函数中申请void*变量 ret = pthread_join(tid,&tret);//传入void*变量的地址。第一个参数不能取地址!!!!
线程的取消
在多线程中,不应该使用信号机制--无法定位递送信号的线程
int pthread_cancel(pthread_t thread);
pthread_cancel
调用该函数,会将目的线程的标志置为真
目标线程正常运行
直到遇到取消点,取消点是一类一类特殊的函数或系统调用,调用完成前,目标线程终止
在线程终止前,会调用资源清理函数,回收所申请的资源
回收thread_local
资源清理函数
cleanup_push cleanip_pop
资源清理栈
当线程去申请一个资源时,把对应的释放行为存入一个栈中 释放行为--->函数指针+参数
每次申请资源时,调用push把对应的释放行为压栈
pop和push必须在同一个作用域成对出现
实现线程的有序退出
取消点----是一个函数
在该函数调用完成前,若有取消点标志位,则该线程终止,基本上是处理文件或引发阻塞的系统调用
close、open、read、write、pthread_join、select、create
pthread_mutex_lock不是取消点
互斥锁
死锁的问题
死锁的四个条件
互斥
该资源只能同时被一个进程访问或占有
请求与保持
一个进程本身占有一种或多种资源,但还有资源没有满足,在等待的同时不释放自己的资源
不可剥夺
不能抢占别人拥有的
循环等待
一个闭环,每个进程都需要下一个进程持有的一种或多种资源
多个资源加锁顺序
避免环形等待
线程持有锁时终止,未释放
持有锁的线程对同一把锁再加锁
锁的使用规范
谁加锁谁解锁
条件变量
一些概念
线程间的同步机制,需要用到互斥锁
可以理解为多进程之间共享的标志位
满足条件运行
不满足就阻塞
条件改变时,通知阻塞在该条件的线程,通知其可以试图运行
条件变量涉及的接口
条件变量本身不涉及条件信息,条件判断不在pthread_cond_wait函数功能中,需要在外面进行判断
条件一般是多线程的共享变量
_cond_wait的底层实现
上半部分陷入
检查是否持有锁
将本线程加入该条件变量的唤醒队列
解锁并陷入阻塞
下半部分恢复
恢复运行并加锁
原子操作 持续有信号过来
执行后续代码
locked--->cond_wait(锁着进入,锁着离开)--->阻塞(unlocked)!!阻塞时会解锁!!--->恢复,加锁---->locked
网络编程模块
OSI/ISO七层模型
物理层
数据链路层
网络层
交换机、路由器也有物理层
传输层
会话层
表示层
应用层
下层为上层提供实现(服务)上层调用下层接口
TCP/IP协议簇
应用层
传输层
TCP:面向连接、可靠
UDP:无连接、不可靠
网络层
网络接口层
协议
一些基本概念
是规则的集合
是水平的
两个对等实体之间
常见的一些协议
应用层
http、HTTPS、ssh(远程连接)、SMTP(电子邮件,发送)、pop3(接收邮件)、FTP(文件传输)
传输层
TCP、UDP、SCTP
网络层
IP、ICMP(控制信息)、IGMP
接口层
ARP地址解析协议---IP到物理地址的映射
私有协议
一般用于游戏、QQ、微信等
公开的
RFC
TCP协议
可靠性
确认
ACK
重传
数据丢了
ACK丢了
引入序号机制即可避免收到重复包
三次握手
C向S发送一个请求,SYN段,该段包含客户端的初始序列号。 SYN=1,ACK = 1seq=x;
S向C回复一个SYN+ACK,包含服务端的初始序列号和回回复已收到了x。 SYN=1,ACK =1 ,seq = y,ack =x + 1
C回复S一个ACK. ACK = 1,seq = x +1,ack = y+1;
上面的seq表示目前已收到的序列号,ack表示期望收到的下一个的序列号
为何三次?
第一次:A给B发,B收到之后可以确定A的发送能力和B的收信能力是没问题的
第二次:B给A发,A收到之后能确认A自己的收发能力的B的收发能力没问题
第三次:A给B发,B收到之后能确认自己的发送能力没问题
如果不是三次握手,改为两次,有可能导致服务器一直在等待客户端发送数据,造成资源浪费。主要考虑到网络原因造成旧的连接请求的SYN对新的连接造成干扰。旧的SYN在连接结束之后再被服务端接收,服务端会以为有个新的请求建立,这时候就会一直等待。
四次挥手
主动断开方向被动方发送一个FIN,seq = u
ESTAB LISTED
ESTAB LISTED
回复一个ACK =1 ,seq = v,ack = u+1
FIN_WAIT1
LOSE_WAIT
被动方回复FIN = 1,ACK = 1,squ = w,ack = u +1
FIN_WAIT2
谁先发起断开谁先发生FIN_WAIT2
LAST_ACK
主动方发送ACK = 1 ,seq = u +1,ack = w +q+1
要等到2MSL再断开
TIME_WAIT CLOSED
CLOSED
为什么不是三次?
全双工通信需要在两端单独关闭。
第一次:主机A向主机B发送FIN包;A告诉B,我(A)发送给你(B)的数据是N,我发送完毕,请求断开A->B的连接。
主机A向主机B发送FIN包;A告诉B,我(A)发送给你(B)的数据大小是N,我发送完毕,请求断开A->B的连接。
主机B向主机A发送FIN包;B告诉A,我(B)发送给你(A)的数据大小是M,我发送完毕,请求断开B->A的连接。
主机A收到了B发送的FIN包,并向主机B发送ACK包;A回答B,是的,我收到了你发送给我的M大小的数据,B->A的连接关闭。
如果三次,那么服务端需要处理最后接收到的信息,这时候就会造成一些影响。
2MSL?
为了保证服务端可以重传一次报文
socet网络编程
一些基础知识
字节序
网络字字节序是大端法
低位存放高位数据
主机字节序是小端法
低位存放低位数据
socket
套接字
这里的字指的是文件描述符
bind
把IP和端口绑定起来
TCP
服务端
创建socket套接字
设置属性,用setsocketopt,可选
绑定ip,端口号等
开启监听,用listen
接收客户端上来的连接,用函数accept()
accept发生在TCP连接的那一次?
和TCP三次握手没关系!!!
TCP三次握手
第一次:connect
第二次第三次:listen
accept是取出一个连接。这时候已经建立好连接了
子主题
收发数据
关闭连接
关闭监听
客户端
创建一个socket套接字 socket()
设置一些属性,用setsocketopt()
绑定IP,端口号等信息到socket上,用bind()
设置要连接的对方的ip,端口等属性
连接服务器,用connect()
收发数据,用send()/recev()或者read()/write()
关闭网络连接
UDP
不需要建立连接,其他类似
注意接收数据用recvfrom、发送数据用sendto
recv只能用于有链接的
from/to有指向
是面向消息的:一发一收
IO多路复用机制:epoll
select的缺点
每次调用select,都需要把fd集合从用户态拷贝到内核态,这个开销在fd很多时会很大
同时每次调用select都需要在内核遍历传递进来的所有fd,这个开销在fd很多时也很大。海量连接,少量就绪时性能差
select支持的文件描述符数量太小了,默认是1024
解决方案:使用epoll
可以处理大量的文件描述符
特别之处
把监听和就绪分离
监听集合位于内核态,循环之后不用重复设置
就绪的在就绪队列中
epoll的实现
三个函数
epoll_create:创建一个epoll句柄
epoll_ctl注册要监听的事件类型
epoll_wait等待事件发生
对于第一个缺点:每次注册新的事件到epoll句柄中时(在epoll_ctl中指定EPOLL_CTL_ADD),会把所有的fd拷贝进内核,而不是在epoll_wait的时候重复拷贝。epoll保证了每个fd在整个过程中只会拷贝一次。
对于第二个缺点,epoll的解决方案不像select或poll一样每次都把current轮流加入fd对应的设备等待队列中,而只在epoll_ctl时把current挂一遍(这一遍必不可少)并为每个fd指定一个回调函数,当设备就绪,唤醒等待队列上的等待者时,就会调用这个回调函数,而这个回调函数会把就绪的fd加入一个就绪链表)。epoll_wait的工作实际上就是在这个就绪链表中查看有没有就绪的fd(利用schedule_timeout()实现睡一会,判断一会的效果,和select实现中的第7步是类似的)。
epoll的使用
epoll_create创建一个epoll文件对象
把所有要监听的fd,用epoll——ctr加入监听
epoll_wait陷入等待,等待之前用一个数组(就绪队列)
遍历就绪队列
目前效率相对较高的是 epoll+ET+非阻塞I/O 模型
ET:边缘触发。缓冲区数据处于上升沿时才触发。可以提高响应能力
选择
对于 fd 数量较少并且 fd IO 都非常繁忙的情况 select 在性能上反而有优势。
进程池和线程池
进程池
优势:一个进程崩溃,不会影响其他的
劣势:内存空间隔离:依赖IPC机制传递文件对象十分困难(sendFd,recvFd);开销很大
进程池的退出
简单粗暴的退出
实现
先fork再signal
signal(SIGUSER1,signFunc).此时遇到10号信号,不会终止,会调用signFunc函数
异步拉起同步
创建一个管道,signal递送则往管道写入一个数据,让epoll去监听读端。 一旦管道读端就绪,就去处理终止子进程的流程
signal是异步的
epoll是同步的
优雅的退出
文件传输
小文件传输
或有粘包问题:
解决方法:
小火车
在应用层构建一个私有协议。规定TCP发送和接收的实际长度进而确定每条消息的边界
火车头存整型数字,用以描述车厢长度
火车车厢承载数据
大文件传输
一个解决思路:发送方用while循环读取内容,装入车厢。接收方不断的使用recv接收小火车,先接收火车头,再根据车厢长度读取内容。
当文件大小增大时,要考虑两个问题
发送的文件和接收的大小不一样:半包问题---因为用了小火车
原因
调用recv的时候会传入一个整型的长度参数,该参数描述的是最大的长度,但实际上recv长度并没有达到最大长度,因为TCP是流式协议,只能保证每个报文可靠有序的发送和接收,但不能保证传到网络缓冲区的是一个完整的小火车。
即不能读取到最大长度
解决
给 recv 函数设置 MSG_WAITALL 属性,这样的话, recv 在不遇 到EOF或者异常关闭的情况就能一定把最大长度数据读取出来。
自己实现recvn
服务器端可能会出现死循环
原因:
服务端的epoll_wait总有文件描述符就绪(该工作进程收到了SIGPIPE信号,变成了僵尸进程(子进程终止而父进程未回收)
解决:
使用signal或者signction忽略该信号
或者send的最后一个参数加上MSG_NOSIGNAL选项
检查文件的正确性
md5码
一种摘要散列算法
128bit,十六个十六进制数
md5sum命令可以生成文件的md5码
若文件相同,则md5码必然相同
零拷贝技术
一些概念
数据拷贝的几种模式
仅CPU
两次拷贝
CPU&DMA
DMA:直接内存访问,绕开CPU独立直接访问内存的设备,一定程度上解放路CPU
两次拷贝
以上两次数据拷贝都需要CPU的参与
零拷贝是为了降低冗余数据拷贝、解放CPU
例子:
传输文件时,要先把磁盘的内容从内核态拷贝到用户态,再从用户态拷贝到内核态。两次拷贝,开销大。
实现方法
mmap+write
mmap是Linux提供的一种内存映射文件的机制,它实现了将内核中读缓冲区地址与用户空间缓冲区地址进行映射,从而实现内核缓冲区与用户缓冲区的共享。
这样就减少了一次用户态和内核态的CPU拷贝,但是在内核空间内仍然有一次CPU拷贝
mmap对大文件传输有一定优势,但是小文件可能出现碎片,并且在多个进程同时操作文件时可能产生引发coredump的signal。
mmap让内核态空间和用户态空间映射到同一片物理内存
此时往mmp写入数据就等于往内核态写入数据
sendfile
让磁盘映射的物理内存和网络的是同一个
把磁盘的内容直接传到网络中去