Linux编程
2021-08-19 16:39:17 0 举报
AI智能生成
linux系统编程 linux网络编程 linuxC/C++网络编程
作者其他创作
大纲/内容
I/O
目录
进程
查看进程状态
ps-eopid,ppid,sid,tty,pgrp,comm,stat|grep-E'bash|PID|进程名'
进程状态
D 不可中断的休眠状态(通常是I/O的进程),可以处理信号,有延迟
R 可执行状态&运行状态(在运行队列里的状态)
S 可中断的休眠状态之中(等待某事件完成),可以处理信号
T 停止或被追踪(被作业控制信号所停止)
Z 僵尸进程
X 死掉的进程
< 高优先级的进程
N 低优先级的进程
L 有些页被锁进内存
s Session leader(进程的领导者),在它下面有子进程
t 追踪期间被调试器所停止
+ 位于前台的进程组
R 可执行状态&运行状态(在运行队列里的状态)
S 可中断的休眠状态之中(等待某事件完成),可以处理信号
T 停止或被追踪(被作业控制信号所停止)
Z 僵尸进程
X 死掉的进程
< 高优先级的进程
N 低优先级的进程
L 有些页被锁进内存
s Session leader(进程的领导者),在它下面有子进程
t 追踪期间被调试器所停止
+ 位于前台的进程组
strace 进程跟踪工具
跟踪进程 pid 所收到的信号
sudostrace-etrace=signal-p pid
sudostrace-etrace=signal-p pid
进程标识
每个进程都有一个非负整型的唯一进程ID
进程ID
pid_t getpid()
父进程ID
pid_t getppid()
获取回话进程ID
pid_t getsid(pid_t pid) // 获取pid的回话进程ID
设置回话进程ID
pid_t setsid() // 设置当前进程在新的session中
创建子进程
pid_t fork()
pid_t fork()
现有进程创建子进程的唯一方法,
创建的子进程开始执行fork()后面与父进程相同的代码
fork 产生的子进程并不复制父进程的内存空间,而是和父进程一起共享一个内存空间,但这个内存空间修改时,那么这个内存就会复制一份给该进程单独使用,以免影响到共享这个内存空间的其他进程
fork函数返回两次,父进程中返回一次,子进程中返回一次;>0 : 父进程; ==0 子进程; <0 : 失败
僵尸进程
当子进程比父进程先结束,而父进程又没有(调用wait/waitpid)回收子进程,系统(init进程)释放子进程占用的资源不完全(没法释放子进程占用的pid资源),此时子进程将成为一个僵尸进程。
解决僵尸进程
SIGCHLD信号处理:处理子进程终止后发送的SIGCHLD信号
1. 父进程中忽略子进程 SIGCHLD信号,有内核回收
signal(SIGCHLD,SIG_IGN)
2. 父进程调用wait/waitpid 等待子进程结束。wait阻塞,waitpid 可以传递WNOHANG 使父进程不阻塞立即返回。
3. 父进程注册SIGCHLD函数,调用wait/waitpid
pid_t wait(int *status)
阻塞,自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止,参数status用来保存被收集进程退出时的一些状态
pid_t waitpid(pid_t pid int *status, int options)
options:允许改变waitpid的行为,最有用的一个选项是WNOHANG,它的作用是防止waitpid把调用者的执行挂起.
pid==-1等待任一子进程。于是在这一功能方面waitpid与wait等效。
pid>0等待其进程ID与pid相等的子进程。
pid==0等待其组ID等于调用进程的组ID的任一子进程。换句话说是与调用者进程同在一个组的进程。
pid<-1等待其组ID等于pid的绝对值的任一子进程
pid==-1等待任一子进程。于是在这一功能方面waitpid与wait等效。
pid>0等待其进程ID与pid相等的子进程。
pid==0等待其组ID等于调用进程的组ID的任一子进程。换句话说是与调用者进程同在一个组的进程。
pid<-1等待其组ID等于pid的绝对值的任一子进程
守护进程
普通进程
ps-eopid,ppid,sid,tty,pgrp,comm,stat,cmd|grep-E'bash|PID|进程名'
进程有对应的终端,终端退出,那么进程也就消失了。
父进程是一个bash
1. 没有控制它的终端(tty),在后台运行
3. 必须是一个session leader
4. 必须是一个进程组的leader
5. root 目录即为工作目录
6. umask 设置为0
源码实例
进程间同步
线程
线程同步
信号
在程序中信号提供一种处理一步事件的方法;
信号名称和编号
以“SIG”开头,定义在signal.h 头文件中,信号都是大于0的正整数
kill -l #查看所有信号
man 7 signal #来查看系统定义
产生信号
按键产生
ctrl+c、ctrl+z、ctrl+\ 等
系统调用产生
kill
int kill(pid_t pid,int sig)
pid >0 : 发送信号给指定进程
pid = 0: 发送信号给调用者同组的所有进程
pid <0: 取|pid|发给对应进程组
成功返回0,失败返回-1
pid >0 : 发送信号给指定进程
pid = 0: 发送信号给调用者同组的所有进程
pid <0: 取|pid|发给对应进程组
成功返回0,失败返回-1
raise
# 给当前进程发送指定信号(自己发给自己)
int raise(int sig);
raise(signo)==kill(getpid(),signo);
int raise(int sig);
raise(signo)==kill(getpid(),signo);
abort
给自己发送异常终止信号6)SIGABRT信号,
void abort(void);无返回
void abort(void);无返回
软件条件产生
定时器 alarm
在指定seconds后,内核会给当前进程发送14)SIGALRM信号。
进程收到该信号,默认动作终止。每个进程都有且只有唯一个定时器
unsigned int alarm(unsigned int seconds); 返回剩余秒数
进程收到该信号,默认动作终止。每个进程都有且只有唯一个定时器
unsigned int alarm(unsigned int seconds); 返回剩余秒数
alarm(0) 取消定时
设置定时器,可替代alarm
int setitimer(int which,const struct itimerval *new_value,struct itimerval* old_value);
which:
自然定时 ITIMER_REAL -> SIGLARM 14
用户空间计时 ITIMER_VIRTUAL -> SIGVTALRM 26
运行时计时 ITIMER_PROF -> SIGPROF 27
int setitimer(int which,const struct itimerval *new_value,struct itimerval* old_value);
which:
自然定时 ITIMER_REAL -> SIGLARM 14
用户空间计时 ITIMER_VIRTUAL -> SIGVTALRM 26
运行时计时 ITIMER_PROF -> SIGPROF 27
硬件异常产生
非法内存访问、除0、内存对齐错误等
命令产生
kill
kill -SIGKILL pid
信号处理方式
信号默认动作
A终止进程
B忽略信号
C进程终止时,会在进程的当前工作目录生产一个core文件,该文件是进程终止时的内存快照,以便以后供debugger调试用。
以下情况不会生产core文件:
(1)为程序设置了set-user-ID并且用户不是程序的所有者;
(2)为程序设置了set-group-ID并且用户不是程序的组所有者;
(3)进程在当前工作目录下面没有写权限;
(4)当前工作目录下已有core文件且进程对该core文件没有写权限;
(5)core文件过大。
D停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)
E信号不能被捕获
F信号不能被忽略
G进程继续(曾被停止的进程)
B忽略信号
C进程终止时,会在进程的当前工作目录生产一个core文件,该文件是进程终止时的内存快照,以便以后供debugger调试用。
以下情况不会生产core文件:
(1)为程序设置了set-user-ID并且用户不是程序的所有者;
(2)为程序设置了set-group-ID并且用户不是程序的组所有者;
(3)进程在当前工作目录下面没有写权限;
(4)当前工作目录下已有core文件且进程对该core文件没有写权限;
(5)core文件过大。
D停止进程,进入停止状况以后还能重新进行下去,一般是在调试的过程中(例如ptrace系统调用)
E信号不能被捕获
F信号不能被忽略
G进程继续(曾被停止的进程)
忽略
大多数信号可以使用这个方式来处理,但是有两种信号不能被忽略(分别是SIGKILL和SIGSTOP)。因为他们向内核和超级用户提供了进程终止和停止的可靠方法,如果忽略了,那么这个进程就变成了没人能管理的的进程。
#include<signal.h>
...
signal(SIGINT,SIG_IGN); // 表示忽略信号SIGINT(SIGKILL、SIGSTOP 不能忽略)
...
signal(SIGINT,SIG_IGN); // 表示忽略信号SIGINT(SIGKILL、SIGSTOP 不能忽略)
信号屏蔽
信号集 sigset_t
每个进程中默认都有一个信号集 sigset;它决定了进程自动屏蔽哪些信号;当注册了某个信号捕捉函数,捕捉到该信号以后,要调用该函数。而该函数有可能执行很长时间,在这期间所屏蔽的信号不由 sigset 来指定。而是用sa_mask来指定。调用完信号处理函数,再恢复为sigset。
XXX信号捕捉函数执行期间,XXX信号自动被屏蔽。
阻塞的常规信号不支持排队,产生多次只记录一次。(后32个实时信号支持排队)
信号屏蔽操作
sigset_t set;//typedef unsigned long sigset_t;
int sigemptyset(sigset_t *set); 将某个信号集清0成功:0;失败:-1
int sigfillset(sigset_t *set); 将某个信号集置1成功:0;失败:-1
int sigaddset(sigset_t *set,int signum); 将某个信号加入信号集成功:0;失败:-1
int sigdelset(sigset_t *set,int signum); 将某个信号清出信号集成功:0;失败:-1
int sigismember(const sigset_t *set,int signum); 判断某个信号是否在信号集中返回值:在集合:1;不在:0;出错:-1
int sigemptyset(sigset_t *set); 将某个信号集清0成功:0;失败:-1
int sigfillset(sigset_t *set); 将某个信号集置1成功:0;失败:-1
int sigaddset(sigset_t *set,int signum); 将某个信号加入信号集成功:0;失败:-1
int sigdelset(sigset_t *set,int signum); 将某个信号清出信号集成功:0;失败:-1
int sigismember(const sigset_t *set,int signum); 判断某个信号是否在信号集中返回值:在集合:1;不在:0;出错:-1
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset); 设置信号集到当前进程;成功:0;失败:-1,设置errno
how参数取值:假设当前的信号屏蔽字为mask
1.SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask=mask|set
2.SIG_UNBLOCK:当how设置为此,set表示需要解除屏蔽的信号。相当于mask=mask&~set
3.SIG_SETMASK:当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask=set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达
how参数取值:假设当前的信号屏蔽字为mask
1.SIG_BLOCK:当how设置为此值,set表示需要屏蔽的信号。相当于mask=mask|set
2.SIG_UNBLOCK:当how设置为此,set表示需要解除屏蔽的信号。相当于mask=mask&~set
3.SIG_SETMASK:当how设置为此,set表示用于替代原始屏蔽及的新屏蔽集。相当于mask=set若,调用sigprocmask解除了对当前若干个信号的阻塞,则在sigprocmask返回前,至少将其中一个信号递达
int sigpending(sigset_t *set); 读取当前进程的未决信号集;set传出参数。返回值:成功:0;失败:-1,设置errno
一个已经产生的信号,但是还没有传递给任何进程,此时该信号的状态就称为未决状态。
一个已经产生的信号,但是还没有传递给任何进程,此时该信号的状态就称为未决状态。
捕捉
说白了就是写一个信号处理函数,然后将这个函数告诉内核。当该信号产生时,由内核来调用用户自定义的函数,以此来实现某种信号的处理。
信号捕捉处理函数
signal
typedef void(*sighandler_t)(int);
# 注册信号signum 到函数 handler, 传SIG_IGN表示忽略
sighandler_t signal(int signum,sighandler_t handler);
# 注册信号signum 到函数 handler, 传SIG_IGN表示忽略
sighandler_t signal(int signum,sighandler_t handler);
sigaction
int sigaction(int signum,const struct sigaction *act,struct sigaction *oldact);
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。
成功:0;失败:-1,设置errno
参数:
act:传入参数,新的处理方式。
oldact:传出参数,旧的处理方式。
成功:0;失败:-1,设置errno
#include<stdio.h>
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void fun(int sig)
{
printf("%d\n",sig);
}
intmain()
{
struct sigaction action;
action.sa_handler=fun;
sigemptyset(&action.sa_mask);
action.sa_flags=0;
sigaction(SIGINT,&action,NULL);
while(1);
}
#include<string.h>
#include<unistd.h>
#include<stdlib.h>
#include<signal.h>
void fun(int sig)
{
printf("%d\n",sig);
}
intmain()
{
struct sigaction action;
action.sa_handler=fun;
sigemptyset(&action.sa_mask);
action.sa_flags=0;
sigaction(SIGINT,&action,NULL);
while(1);
}
struct sigaction{
void(*sa_handler)(int);
void(*sa_sigaction)(int,siginfo_t*,void*);
sigset_tsa_mask;
intsa_flags;
void(*sa_restorer)(void);
};
void(*sa_handler)(int);
void(*sa_sigaction)(int,siginfo_t*,void*);
sigset_tsa_mask;
intsa_flags;
void(*sa_restorer)(void);
};
sa_restorer:该元素是过时的,不应该使用,POSIX.1标准将不指定该元素。(弃用)
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作
sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为0,表使用默认属性。
sa_sigaction:当sa_flags被指定为SA_SIGINFO标志时,使用该信号处理程序。(很少使用)
sa_handler:指定信号捕捉后的处理函数名(即注册函数)。也可赋值为SIG_IGN表忽略或SIG_DFL表执行默认动作
sa_mask: 调用信号处理函数时,所要屏蔽的信号集合(信号屏蔽字)。注意:仅在处理函数被调用期间屏蔽生效,是临时性设置。
sa_flags:通常设置为0,表使用默认属性。
可/不可重入函数
一个函数在被调用执行期间(尚未调用结束),由于某种时序又被重复调用,称之为“重入”。
显然,insert函数是不可重入函数,重入调用,会导致意外结果呈现。究其原因,是该函数内部实现使用了全局变量
1.定义可重入函数,函数内不能含有全局变量及static变量,不能使用malloc、free
2.信号捕捉函数应设计为可重入函数
3.信号处理程序可以调用的可重入函数可参阅man7signal
4.没有包含在上述列表中的函数大多是不可重入的,其原因为:
a)使用静态数据结构
b)调用了malloc或free
c)是标准I/O函数
网络
netstat 显示网络相关信息
# 查看 9000端口状态
netstat -anp | grep 'State|9000'
netstat -anp | grep 'State|9000'
TCP
协议简述
TCP 提供面向有连接的通信传输,面向有连接是指在传送数据之前必须先建立连接,数据传送完成后要释放连接。
无论哪一方向另一方发送数据之前,都必须先在双方之间建立一条连接。在TCP/IP协议中,TCP协议提供可靠的连接服务,连接是通过三次握手进行初始化的。
同时由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式,所以需要四次挥手关闭连接。
TCP数据包的封装
数据传输过程
TCP包封装
TCP端口号
TCP的连接是需要四个要素确定唯一一个连接:
(源IP,源端口号)+ (目地IP,目的端口号)
所以TCP首部预留了两个16位作为端口号的存储,而IP地址由上一层IP协议负责传递源端口号和目地端口各占16位两个字节,也就是端口的范围是2^16=65535;另外1024以下是系统保留的,从1024-65535是用户使用的端口范围
TCP的序号和确认号:
1. 32位序号 seq:Sequence number 缩写seq ,TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为1000,发送了1000,下一个序列号就是2000。
2. 32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。
TCP的标志位
每个TCP段都有一个目的,这是借助于TCP标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。
用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。
TCP的连接是需要四个要素确定唯一一个连接:
(源IP,源端口号)+ (目地IP,目的端口号)
所以TCP首部预留了两个16位作为端口号的存储,而IP地址由上一层IP协议负责传递源端口号和目地端口各占16位两个字节,也就是端口的范围是2^16=65535;另外1024以下是系统保留的,从1024-65535是用户使用的端口范围
TCP的序号和确认号:
1. 32位序号 seq:Sequence number 缩写seq ,TCP通信过程中某一个传输方向上的字节流的每个字节的序号,通过这个来确认发送的数据有序,比如现在序列号为1000,发送了1000,下一个序列号就是2000。
2. 32位确认号 ack:Acknowledge number 缩写ack,TCP对上一次seq序号做出的确认号,用来响应TCP报文段,给收到的TCP报文段的序号seq加1。
TCP的标志位
每个TCP段都有一个目的,这是借助于TCP标志位选项来确定的,允许发送方或接收方指定哪些标志应该被使用,以便段被另一端正确处理。
用的最广泛的标志是 SYN,ACK 和 FIN,用于建立连接,确认成功的段传输,最后终止连接。
- SYN:简写为S,同步标志位,用于建立会话连接,同步序列号;
- ACK: 简写为.,确认标志位,对已接收的数据包进行确认;
- FIN: 简写为F,完成标志位,表示我已经没有数据要发送了,即将关闭连接;
- PSH:简写为P,推送标志位,表示该数据包被对方接收后应立即交给上层应用,而不在缓冲区排队;
- RST:简写为R,重置标志位,用于连接复位、拒绝错误和非法的数据包;
- URG:简写为U,紧急标志位,表示数据包的紧急指针域有效,用来保证连接不被阻断,并督促中间设备尽快处理;
TCP三次握手建立
所谓三次握手(Three-way Handshake),是指建立一个 TCP 连接时,需要客户端和服务器总共发送3个报文。
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。
三次握手的目的是连接服务器指定端口,建立 TCP 连接,并同步连接双方的序列号和确认号,交换 TCP 窗口大小信息。在 socket 编程中,客户端执行 connect() 时。将触发三次握手。
第一次握手:
第二次握手:
第三次握手:
- 客户端将TCP报文标志位SYN置为1,随机产生一个序号值seq=J,保存在TCP首部的序列号(Sequence Number)字段里,指明客户端打算连接的服务器的端口,并将该数据包发送给服务器端,发送完毕后,客户端进入SYN_SENT状态,等待服务器端确认。
第二次握手:
- 服务器端收到数据包后由标志位SYN=1知道客户端请求建立连接,服务器端将TCP报文标志位SYN和ACK都置为1,ack=J+1,随机产生一个序号值seq=K,并将该数据包发送给客户端以确认连接请求,服务器端进入SYN_RCVD状态。
第三次握手:
- 客户端收到确认后,检查ack是否为J+1,ACK是否为1,如果正确则将标志位ACK置为1,ack=K+1,并将该数据包发送给服务器端,服务器端检查ack是否为K+1,ACK是否为1,如果正确则连接建立成功,客户端和服务器端进入ESTABLISHED状态,完成三次握手,随后客户端与服务器端之间可以开始传输数据了。
抓包实例分析
第一次握手:
fce0 : 64736 (16位源端口)
1f40 : 8000 (16位目的端口)
cf249414 : 3475280916 (32位序号)
00000000 :0 (32位确认号)
8 : 8 (4位头部长度)
002 :000000000010 (6位保留和6位标识 'SYN')
ffff : 65535 16位窗口大小
ba3e :16位校验和
0000 :16位紧急指针
02 04 ff d7 01 03 03 08 01 01 04 02 :IP包数据
fce0 : 64736 (16位源端口)
1f40 : 8000 (16位目的端口)
cf249414 : 3475280916 (32位序号)
00000000 :0 (32位确认号)
8 : 8 (4位头部长度)
002 :000000000010 (6位保留和6位标识 'SYN')
ffff : 65535 16位窗口大小
ba3e :16位校验和
0000 :16位紧急指针
02 04 ff d7 01 03 03 08 01 01 04 02 :IP包数据
第二次握手:
- 由服务端端口到客户端端口
- 服务器端将TCP报文标志位 SYN和ACK都置为 1
- 生成服务端序号 590af0e8
- 32位确认号 是第一次握手的32位序号+1
第三次握手:
- 32位序号为第二次握手的32位确认号(第一次握手的序号+1)
- 32位确认号 为 第二次握手的 32位序号+1
- ACK 置为 1
为什么需要三次握手
我们假设client发出的第一个连接请求报文段并没有丢失,而是在某个网络结点长时间的滞留了,以致延误到连接释放以后的某个时间才到达server。
本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
所以,采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。
TCP 三次握手跟现实生活中的人与人打电话是很类似的:
三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天 balabala……”
经过三次的互相确认,大家就会认为对方对听的到自己说话,并且愿意下一步沟通,否则,对话就不一定能正常下去了。
本来这是一个早已失效的报文段。但server收到此失效的连接请求报文段后,就误认为是client再次发出的一个新的连接请求。于是就向client发出确认报文段,同意建立连接。
假设不采用“三次握手”,那么只要server发出确认,新的连接就建立了。由于现在client并没有发出建立连接的请求,因此不会理睬server的确认,也不会向server发送数据。但server却以为新的运输连接已经建立,并一直等待client发来数据。这样,server的很多资源就白白浪费掉了。
所以,采用“三次握手”的办法可以防止上述现象发生。例如刚才那种情况,client不会向server的确认发出确认。server由于收不到确认,就知道client并没有要求建立连接。
TCP 三次握手跟现实生活中的人与人打电话是很类似的:
三次握手:
“喂,你听得到吗?”
“我听得到呀,你听得到我吗?”
“我能听到你,今天 balabala……”
经过三次的互相确认,大家就会认为对方对听的到自己说话,并且愿意下一步沟通,否则,对话就不一定能正常下去了。
TCP四次挥手关闭连接
挥手请求可以是Client端,也可以是Server端发起的,我们假设是Client端发起:
第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。
第一次挥手: Client端发起挥手请求,向Server端发送标志位是FIN报文段,设置序列号seq,此时,Client端进入FIN_WAIT_1状态,这表示Client端没有数据要发送给Server端了。
第二次分手:Server端收到了Client端发送的FIN报文段,向Client端返回一个标志位是ACK的报文段,ack设为seq加1,Client端进入FIN_WAIT_2状态,Server端告诉Client端,我确认并同意你的关闭请求。
第三次分手: Server端向Client端发送标志位是FIN的报文段,请求关闭连接,同时Client端进入LAST_ACK状态。
第四次分手 : Client端收到Server端发送的FIN报文段,向Server端发送标志位是ACK的报文段,然后Client端进入TIME_WAIT状态。Server端收到Client端的ACK报文段以后,就关闭连接。此时,Client端等待2MSL的时间后依然没有收到回复,则证明Server端已正常关闭,那好,Client端也可以关闭连接了。
为什么连接的时候是三次握手,关闭的时候却是四次握手?
建立连接时因为当Server端收到Client端的SYN连接请求报文后,可以直接发送SYN+ACK报文。其中ACK报文是用来应答的,SYN报文是用来同步的。所以建立连接只需要三次握手。
由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。
这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了,但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。
当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
由于TCP协议是一种面向连接的、可靠的、基于字节流的运输层通信协议,TCP是全双工模式。
这就意味着,关闭连接时,当Client端发出FIN报文段时,只是表示Client端告诉Server端数据已经发送完毕了。当Server端收到FIN报文并返回ACK报文段,表示它已经知道Client端没有数据发送了,但是Server端还是可以发送数据到Client端的,所以Server很可能并不会立即关闭SOCKET,直到Server端把数据也发送完毕。
当Server端也发送了FIN报文段时,这个时候就表示Server端也没有数据要发送了,就会告诉Client端,我也没有数据要发送了,之后彼此就会愉快的中断这次TCP连接。
为什么要等待2MSL?
MSL:报文段最大生存时间,它是任何报文段被丢弃前在网络内的最长时间。
有以下两个原因:
第一点:保证TCP协议的全双工连接能够可靠关闭:
由于IP协议的不可靠性或者是其它网络原因,导致了Server端没有收到Client端的ACK报文,那么Server端就会在超时之后重新发送FIN,如果此时Client端的连接已经关闭处于CLOESD状态,那么重发的FIN就找不到对应的连接了,从而导致连接错乱,所以,Client端发送完最后的ACK不能直接进入CLOSED状态,而要保持TIME_WAIT,当再次收到FIN的收,能够保证对方收到ACK,最后正确关闭连接。
第二点:保证这次连接的重复数据段从网络中消失
如果Client端发送最后的ACK直接进入CLOSED状态,然后又再向Server端发起一个新连接,这时不能保证新连接的与刚关闭的连接的端口号是不同的,也就是新连接和老连接的端口号可能一样了,那么就可能出现问题:如果前一次的连接某些数据滞留在网络中,这些延迟数据在建立新连接后到达Client端,由于新老连接的端口号和IP都一样,TCP协议就认为延迟数据是属于新连接的,新连接就会接收到脏数据,这样就会导致数据包混乱。所以TCP连接需要在TIME_WAIT状态等待2倍MSL,才能保证本次连接的所有数据在网络中消失。
有以下两个原因:
第一点:保证TCP协议的全双工连接能够可靠关闭:
由于IP协议的不可靠性或者是其它网络原因,导致了Server端没有收到Client端的ACK报文,那么Server端就会在超时之后重新发送FIN,如果此时Client端的连接已经关闭处于CLOESD状态,那么重发的FIN就找不到对应的连接了,从而导致连接错乱,所以,Client端发送完最后的ACK不能直接进入CLOSED状态,而要保持TIME_WAIT,当再次收到FIN的收,能够保证对方收到ACK,最后正确关闭连接。
第二点:保证这次连接的重复数据段从网络中消失
如果Client端发送最后的ACK直接进入CLOSED状态,然后又再向Server端发起一个新连接,这时不能保证新连接的与刚关闭的连接的端口号是不同的,也就是新连接和老连接的端口号可能一样了,那么就可能出现问题:如果前一次的连接某些数据滞留在网络中,这些延迟数据在建立新连接后到达Client端,由于新老连接的端口号和IP都一样,TCP协议就认为延迟数据是属于新连接的,新连接就会接收到脏数据,这样就会导致数据包混乱。所以TCP连接需要在TIME_WAIT状态等待2倍MSL,才能保证本次连接的所有数据在网络中消失。
TCP状态
客户端发送了FIN,但是没有收到服务器的ACK,却收到了服务器的FIN,这种情况发生在服务器发送的ACK丢包的时候,因为网络传输有时会有意外。
11 种状态
LISTEN:等待从任何远端TCP 和端口的连接请求。
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
SYN_SENT:发送完一个连接请求后等待一个匹配的连接请求。
SYN_RECEIVED:发送连接请求并且接收到匹配的连接请求以后等待连接请求确认。
ESTABLISHED:表示一个打开的连接,接收到的数据可以被投递给用户。连接的数据传输阶段的正常状态。
FIN_WAIT_1:等待远端TCP 的连接终止请求,或者等待之前发送的连接终止请求的确认。
FIN_WAIT_2:等待远端TCP 的连接终止请求。
CLOSE_WAIT:等待本地用户的连接终止请求。
CLOSING:等待远端TCP 的连接终止请求确认。
LAST_ACK:等待先前发送给远端TCP 的连接终止请求的确认(包括它字节的连接终止请求的确认)
TIME_WAIT:等待足够的时间过去以确保远端TCP 接收到它的连接终止请求的确认。
TIME_WAIT 两个存在的理由:
1.可靠的实现tcp全双工连接的终止;
2.允许老的重复分节在网络中消逝。
CLOSED:不在连接状态(这是为方便描述假想的状态,实际不存在)
监听套接字队列
int listen(int sockfd, int backlog);
调用listen()进行监听的套接字,操作系统会给这个套接字 维护两个队列;
a. 未完成连接队列
当客户端 发送tcp连接给服务器时,服务器创建此队列。
b. 已完成连接队列
完成三次握手后,把未完成队列中的socket移动到此队列。
backlog
原含义 :已完成队列和未完成队列里边条目之和 不能超过 backlog;
新含义 : 已完成队列的条目
RTT
客户端,这个RTT时间是第一次和第二次握手加起来的时间;
服务器,这个RTT时间实际上是第二次和第三次握手加起来的时间
阻塞与非阻塞
调用函数获取数据,系统没有满足条件的数据时,进程进入休眠,默认的socket 是阻塞的。
调用函数获取数据,系统无满足条件的数据时,返回错误标识;
int fcntl(int fd, int cmd, ... /* arg */ );
fd : IO
cmd : F_GETFL(获取IO标识)和F_SETFL(设置文件标志)
fd : IO
cmd : F_GETFL(获取IO标识)和F_SETFL(设置文件标志)
- O_NONBLOCK 非阻塞I/O;如果read(2)调用没有可读取的数据,或者如果write(2)操作将阻塞,read或write调用返回-1和EAGAIN错误
- O_APPEND 强制每次写(write)操作都添加在文件大的末尾,相当于open(2)的O_APPEND标志
- O_DIRECT 最小化或去掉reading和writing的缓存影响.系统将企图避免缓存你的读或写的数据. 如果不能够避免缓存,那么它将最小化已经被缓存了的数 据造成的影响.如果这个标志用的不够好,将大大的降低性能
- O_ASYNC 当I/O可用的时候,允许SIGIO信号发送到进程组,例如:当有数据可以读的时候
/* 设置 socket 属性为非阻塞方式 */
if(fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {
perror("fcntl");
exit(errno);
}
if(fcntl(sockfd, F_SETFL, O_NONBLOCK) == -1) {
perror("fcntl");
exit(errno);
}
同步IO与异步IO
异步I/O:
- 调用一个异步I/O函数时,我门要给这个函数指定一个接收缓冲区,我还要给定一个回调函数;调用完一个异步I/O函数后,该函数会立即返回。 其余判断交给操作系统,操作系统会判断数据是否到来,如果数据到来了,操作系统会把数据拷贝到你所提供的缓冲区里,然后调用你所指定的这个回调函数来通知你;
同步I/O:
select()返回之后,用recvfrom()去取数据
- select/poll :
select()返回之后,用recvfrom()去取数据
I/O复用:
用select这种同步I/O函数处理多个连接,只要其中一个连接有数据到来,就返回,用recvfrom 去去数据
用select这种同步I/O函数处理多个连接,只要其中一个连接有数据到来,就返回,用recvfrom 去去数据
- 建立连接之前服务器和客户端的状态都为 CLOSED;
- 服务器创建 Socket后开始监听, 服务端变为 LISTEN 监听状态;
- 客户端请求建立连接,向服务器发送 SYN 报文,客户端状态变为 SYN_SENT;
- 服务器收到客户端的报文后向客户端发送 ACK和SYN报文,此时服务器状态变为 SYN_RCVD;
- 客户端收到ACK 、SYN后,就向服务端发送ACK,客户端状态变为 ESTABLISHED;
- 服务器收到客户端的ACK后 变为 ESTABLISHED,状态,此时3次握手完成。
- 客户端先向服务器发送FIN报文,请求断开连接,其状态变为FIN_WAIT1;
- 服务器收到FIN后向客户端发送ACK,服务器的状态围边CLOSE_WAIT;
- 客户端收到ACK后就进入FIN_WAIT2状态,此时连接已经断开了一半了。如果服务器还有数据要发送给客户端,就会继续发送;
- 直到发完数据,就会发送FIN报文,此时服务器进入LAST_ACK状态;
- 客户端收到服务器的FIN后,马上发送ACK给服务器,此时客户端进入TIME_WAIT状态;
- 再过了2MSL长的时间后进入CLOSED状态。服务器收到客户端的ACK就进入CLOSED状态。
- 至此,还有一个状态没有出来:CLOSING状态。
客户端发送了FIN,但是没有收到服务器的ACK,却收到了服务器的FIN,这种情况发生在服务器发送的ACK丢包的时候,因为网络传输有时会有意外。
epoll
epoll原理
int epoll_create(int size);
创建一个代表该 Epoll 的 eventpoll 对象,
rbr : 监视列表(红黑树)
rdlist:就绪列表(引用收到数据的Socket)
rdlist:就绪列表(引用收到数据的Socket)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
维护监视列表(增加、删除、修改 Socket)
op 参数:
EPOLL_CTL_ADD:向interest list添加一个需要监视的描述符
EPOLL_CTL_DEL:从interest list中删除一个描述符
EPOLL_CTL_MOD:修改interest list中一个描述符
EPOLL_CTL_ADD:向interest list添加一个需要监视的描述符
EPOLL_CTL_DEL:从interest list中删除一个描述符
EPOLL_CTL_MOD:修改interest list中一个描述符
event 参数:
epoll的两种触发方式
水平触发的时机
对于读操作,只要缓冲内容不为空,LT模式返回读就绪。
对于写操作,只要缓冲区还不满,LT模式会返回写就绪。
边缘触发的时机
当缓冲区由不可读变为可读的时候,即缓冲区由空变为不空的时候。
当有新数据到达时,即缓冲区中的待读数据变多的时候。
当缓冲区有数据可读,且应用进程对相应的描述符进行
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
阻塞等待注册的事件发生,返回事件的数目,并将触发的事件写入events数组中。
events: 用来记录被触发的events,其大小应该和maxevents一致
maxevents: 返回的events的最大个数
timeout 参数:
timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时
timeout = -1表示调用将一直阻塞,直到有文件描述符进入ready状态或者捕获到信号才返回;
timeout = 0用于非阻塞检测是否有描述符处于ready状态,不管结果怎么样,调用都立即返回;
timeout > 0表示调用将最多持续timeout时间,如果期间有检测对象变为ready状态或者捕获到信号则返回,否则直到超时
工作流程
调用 epoll_create 创建一个eventpoll对象(维护一个监控列表和就绪列表)
epoll_ctl 维护监控空列表
接收数据
Socket 收到数据后,中断程序会给 eventpoll 的“就绪列表”添加 Socket 引用
阻塞和唤醒进程
程序执行到 epoll_wait 时,如果 Rdlist 已经引用了 Socket,那么 epoll_wait 直接返回,如果 Rdlist 为空,阻塞进程
当 Socket 接收到数据,中断程序一方面修改 Rdlist,另一方面唤醒 eventpoll 等待队列中的进程,进程 A 再次进入运行状态
epoll设计思想
一、功能分离
Select 低效的原因之一是将“维护等待队列”和“阻塞进程”两个步骤合二为一
每次调用 Select 都需要这两步操作,然而大多数应用场景中,需要监视的 Socket 相对固定,并不需要每次都修改
Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升
Epoll 将这两个操作分开,先用 epoll_ctl 维护等待队列,再调用 epoll_wait 阻塞进程。显而易见地,效率就能得到提升
先用 epoll_create 创建一个 Epoll 对象 Epfd,再通过 epoll_ctl 将需要监视的 Socket 添加到 Epfd 中,最后调用 epoll_wait 等待数据
二、就绪列表
Select 低效的另一个原因在于程序不知道哪些 Socket 收到数据,只能一个个遍历。如果内核维护一个“就绪列表”,引用收到数据的 Socket,就能避免遍历。
计算机共有三个 Socket,收到数据的 Sock2 和 Sock3 被就绪列表 Rdlist 所引用。
当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据
当进程被唤醒后,只要获取 Rdlist 的内容,就能够知道哪些 Socket 收到数据
Select设计思想
假如能够预先传入一个 Socket 列表,如果列表中的 Socket 都没有数据,挂起进程,直到有一个 Socket 收到数据,唤醒进程。
1. 准备一个数组 FDS,让 FDS 存放着所有需要监视的 Socket。
2. 调用 Select,如果 FDS 中的所有 Socket 都没有数据,Select 会阻塞,直到有一个 Socket 接收到数据,Select 返回,唤醒进程。
3. 用户可以遍历 FDS,通过 FD_ISSET 判断具体哪个 Socket 收到数据,然后做出处理。
2. 调用 Select,如果 FDS 中的所有 Socket 都没有数据,Select 会阻塞,直到有一个 Socket 接收到数据,Select 返回,唤醒进程。
3. 用户可以遍历 FDS,通过 FD_ISSET 判断具体哪个 Socket 收到数据,然后做出处理。
Select 的实现思路很直接,假如程序同时监视如下图的 Sock1、Sock2 和 Sock3 三个 Socket,那么在调用 Select 之后,操作系统把进程 A 分别加入这三个 Socket 的等待队列中。
操作系统把进程 A 分别加入这三个 Socket 的等待队列中。
当任何一个 Socket 收到数据后,中断程序将唤起进程。下图展示了 Sock2 接收到了数据的处理流程:
操作系统把进程 A 分别加入这三个 Socket 的等待队列中。
当任何一个 Socket 收到数据后,中断程序将唤起进程。下图展示了 Sock2 接收到了数据的处理流程:
Sock2 接收到了数据,中断程序唤起进程 A。
所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面
注: 当程序调用 Select 时,内核会先遍历一遍 Socket,如果有一个以上的 Socket 接收缓冲区有数据,那么 Select 直接返回,不会阻塞。
所谓唤起进程,就是将进程从所有的等待队列中移除,加入到工作队列里面
注: 当程序调用 Select 时,内核会先遍历一遍 Socket,如果有一个以上的 Socket 接收缓冲区有数据,那么 Select 直接返回,不会阻塞。
将进程 A 从所有等待队列中移除,再加入到工作队列里面
经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。
经由这些步骤,当进程 A 被唤醒后,它知道至少有一个 Socket 接收了数据。程序只需遍历一遍 Socket 列表,就可以得到就绪的 Socket。
这种简单方式行之有效,在几乎所有操作系统都有对应的实现。但是简单的方法往往有缺点,主要是:
1. 每次调用 Select 都需要将进程加入到所有监视 Socket 的等待队列,每次唤醒都需要从每个队列中移除。这里涉及了两次遍历,而且每次都要将整个 FDS 列表传递给内核,有一定的开销。
2. 进程被唤醒后,程序并不知道哪些 Socket 收到数据,还需要遍历一次。
服务端需要管理多个客户端连接,而 Recv 只能监视单个 Socket,这种矛盾下,人们开始寻找监视多个 Socket 的方法;
工作队列
计算机中运行着 A、B 与 C 三个进程,其中进程 A 执行着上述基础网络程序,一开始,这 3 个进程都被操作系统的工作队列所引用,处于运行状态,会分时执行。
当进程 A 执行到创建 Socket 的语句时,操作系统会创建一个由文件系统管理的 Socket 对象(如下图)
这个 Socket 对象包含了发送缓冲区、接收缓冲区与等待队列等成员。等待队列是个非常重要的结构,它指向所有需要等待该 Socket 事件的进程。
当程序执行到 Recv 时,操作系统会将进程 A 从工作队列移动到该 Socket 的等待队列中
由于工作队列只剩下了进程 B 和 C,依据进程调度,CPU 会轮流执行这两个进程的程序,不会执行进程 A 的程序。所以进程 A 被阻塞,不会往下执行代码,也不会占用 CPU 资源。
注:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。
注:操作系统添加等待队列只是添加了对这个“等待中”进程的引用,以便在接收到数据时获取进程对象、将其唤醒,而非直接将进程管理纳入自己之下。上图为了方便说明,直接将进程挂到等待队列之下。
当 Socket 接收到数据后,操作系统将该 Socket 等待队列上的进程重新放回到工作队列,该进程变成运行状态,继续执行代码。
同时由于 Socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据。
同时由于 Socket 的接收缓冲区已经有了数据,Recv 可以返回接收到的数据。
唤醒队列
首先从硬件分析
1. 网卡接收到网线传来的数据
2. 通过硬件DMA、IO传输到内存中
3. 内存保存数据到缓冲区
4. 网卡传输完成后,向cpu发送一个中断信号,cpu就知道了信数据到来,并处理
2. 通过硬件DMA、IO传输到内存中
3. 内存保存数据到缓冲区
4. 网卡传输完成后,向cpu发送一个中断信号,cpu就知道了信数据到来,并处理
0 条评论
下一页