Liunx之网络编程

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/zhaoyinhui0802/article/details/89337961

进程

进程:资源分配的最小单元,操作系统执行的最小单位;
Linux对进程采用了一种层次系统,每个进程都依赖于一个父进程。内核启动init程序作为第一个进程,该进程负责进一步的系统初始化操作,并显示登录提示符或图形登录界面(现在使用比较广泛)。因此init是进程树的根,所有进程都直接或间接起源自该进程。
fork()函数可以创建当前进程的一个副本,父进程和子进程只有PID(进程ID)不同。
exec将一个新程序加载到当前进程的内存中并执行。旧程序的内存页将刷出,其内容将替换为新的数据。然后开始执行新程序。
Liunx系统构架
Liunx的内核主要由五个子系统组成:进程调度、内存管理、虚拟文件系统、网络接口、进程间通信。
进程调度SCHED
Liunx下的进程调度有三种策略:SCHED_OTHER、SCHED_FIFO、SCHED_RR。
SCHED_OTHER是针对普通进程的时间片轮换调度策略。这种策略中,系统给所有的运行状态的进程分配时间片。在当前进程的时间片用完之后,系统从进程中优先级最高的进程中选择进程运行。
SCHED_FIFO是针对运行的实时性2要求比较高、运行时间短的进程调度策略。这种策略中,系统按照进入队列的先后进行进程的调度,在没有更高优先级进程到来或者当前进程没有因为资源等待而阻塞的情况下,会一直运行。
SCHED_RR是针对实时性要求比较高、运行时间比较长的进程调度策略。这种策略与SCHED_OTHER策略类似,只不过SCHED_RR进程的优先级要高很多。系统分配给SCHED_RR进程时间片,然后轮询进行这些进程,将时间片用完的进程放入队列的末尾。
由于存在多种调度方式,Liunx进程调度采用的是“有条件可剥夺”的调度方式、普通进程中采用的是SCHED_OTHER的时间片轮询方式,实时进程可以剥夺普通进程。如果普通进程在用户空间中运行,则普通进程立刻停止,将资源让给实时进程;如果普通进程运行在内核中,则需要等待系统调用返回用户空间后方可剥夺资源。
线程:资源调度的最小单元,可以在同一个进程中共享资源的一个执行单位。
Linux用clone方法创建线程。其工作方式类似于fork,但启用了精确的检查,以确认哪些资源与父进程共享、哪些资源为线程独立创建。
进程的产生过程
1、首先复制其父进程的环境配置;2、在内核中建立进程结构;3、讲结构插入到进程列表,便于维护;4、分配资源给此进程;5、复制父进程的内存消息映射;6、管理文件描述符和链接;7、通知父进程。
fork生成当前进程的一个相同副本,该副本称之为子进程。原进程的所有资源都以适当的方式复制到子进程,因此该系统调用之后,原来的进程就有了两个独立的实例。这两个实例的联系包括:同一组打开文件、同样的工作目录、内存中同样的数据(两个进程各有一份副本),等等。此外二者别无关联。 ①
exec从一个可执行的二进制文件加载另一个应用程序,来代替当前运行的进程。换句话说,加载了一个新程序。因为exec并不创建新进程,所以必须首先使用fork复制一个旧的程序,然后调用exec在系统上创建另一个应用程序。
进程的3种状态
1、运行:该进程此刻正在执行。
2、等待:进程能够运行,但没有得到许可,因为CPU分配给另一个进程。调度器可以在下一次任务切换时选择该进程。## 标题 ##
3、睡眠:进程正在睡眠无法运行,因为它在等待一个外部事件。调度器无法在下一次任务切换时选择该进程。
Linux进程管理的结构中还需要另外两种进程状态选项:用户状态和核心态。
用户状态转换为核心态的两种方式:系统调用和中断。
进程的终止过程(5种)
1、从main返回;2、调用exit;3调用_exit;4、调用abort;5、有一个信号终止;
进程之间的通信方式
1、管道(PIPE)(某一个进程的输出(后一个进程)和另一个进程(前一个进程)的输入相连接)
管道操作是阻塞性质的;
管道创建函数pipe()
int pipe(int filedes[2]);
fd0是为了写操作而创建和打开的,fd1是为了读操作而创建和打开的。fd1的输出是fd0的输入;
管道内部传输的数据是字节流,和TCP字节流的概念相同,但是也有区别。应用层能往一个TCP连接中写入多少字节的数据,取决于对方接受通告的窗口大小和本段的窗口大小。而管道本身拥有一个容量限制,他规定如果应用程序不将数据从管道读走的话,改管道最多能够写入多少字节数据。
有名管道:一种半双工的通信方式,它允许无亲缘关系进程间的通信
创建方式:(1)、直接用shell;(2)、使用mkfifo()函数;

   int mkfifo(const char *pathname,mode_t mode);

在FIFO中,必须使用一个open()函数显式的建立连接到管道的通道。一般来说FIFO总是处于阻塞状态。
优点:可以实现任意关系的进程间的通信
缺点:1、长期存于系统中,使用不当容易出错;2、缓冲区有限
无名管道:一种半双工的通信方式,只能在具有亲缘关系的进程间使用(父子进程)
优点:简单方便
缺点:1、局限于单向通信;2、只能创建在它的进程以及其有亲缘关系的进程之间;3、缓冲区有限
2、信号量(Semaphore):一个计数器,可以用来控制多个线程对共享资源的访问
优点:可以同步进程
缺点:信号量有限
3、信号(Signal):一种比较复杂的通信方式,用于通知接收进程某个事件已经发生
4、消息队列(Message Queue):
是消息的链表,存放在内核中并由消息队列标识符标识(异步操作方式)。
优点:可以实现任意进程间的通信,并通过系统调用函数来实现消息发送和接收之间的同步,无需考虑同步问题,方便
缺点:信息的复制需要额外消耗 CPU 的时间,不适宜于信息量大或操作频繁的场合
5、共享内存(Shared Memory):映射一段能被其他进程所访问的内存,这段共享内存由一个进程创建,但多个进程都可以访问
优点:无须复制,快捷,信息量大
缺点:1、通信是通过将共享空间缓冲区直接附加到进程的虚拟地址空间中来实现的,因此进程间的读写操作的同步问题;2、利用内存缓冲区直接交换信息,内存的实体存在于计算机中,只能同一个计算机系统中的诸多进程共享,不方便网络通信
6、套接字(Socket):可用于不同及其间的进程通信
优点:1、传输数据为字节级,传输数据可自定义,数据量小效率高;2、传输数据时间短,性能高;3、适合于客户端和服务器端之间信息实时交互;4、可以加密,数据安全性强
缺点:需对传输的数据进行解析,转化成应用级的数据。
二、进程的产生方式(fork(),system(),exec()
在Liunx系统中,除了初始进程init,所有的进程基本上都是父子或者堂兄关系的,没有那个进程与其他进程完全独立。每个进程都有一个父进程,新的进程不是被全新的创建,通常是从一个原有的进程进行复制或者克隆。
进程号:进程初始化时系统分配的ID号,用于标识进程。称为PID,变量类型为pid_t。
函数getpid()用于返回当前进程的ID;
函数getppid()用于返回父进程的ID;
进程复制fork()
以父进程为蓝本复制一个进程,其ID和父进程ID不同。Liunx环境下以写方式实现,只有在内存等于父进程不同,其他与父进程共享。
pid_t fork(void);
成功时,返回进程的ID;失败返回-1;
system()
调用shell的外部命令(/bin/sh -c command,阻塞当前进程知道command命令执行完毕)在当前进程中开始另一个进程。

int system(const char* command);

返回值:失败,返回-1;sh不能执行,返回127;成功,返回进程状态值;
exec()函数系列
作用:在原来的进程内部运行一个可执行文件。
与fork()的不同:exce()系列函数执行成功后不会有返回值,因为执行的新进程占用了当前进程的空间和资源;失败返回-1;
解决方法:先用fork()函数分叉进程,然后在新的进程中调用exec()函数,这样exec()函数会占用和原来一样的系统资源运行。

线程

(1)、创建线程函数pthread_create()

int pthread_create(pthread_t *thread,pthread_attr_t *attr,void * (*start_rountine)(void *),void * arg);
thread:用于标识一个线程;
attr:设置线程的属性,默认为NULL;
start_routine:当现成的资源分配成功后,线程中所运行的单元;
arg:线程函数运行时传入的参数;
返回值:成功返回1,不成功返回0;EAGAIN表示线程数量达到上限;EINVAL表示线程的属性非法;

(2)、线程结束函数pthread_join()pthread_exit()
函数pthread_join()用来等待一个线程运行结束,是阻塞函数,一直等到线程结束为止,函数才返回并且回收被等待线程的资源。

extern int pthread_join _P (pthread_t _th,void ** _thread_return);
_th:线程的标识,即pthread_create()函数创建成功返回的值;
_thread_return:线程返回值,是一个指针,可以用来存储被等待线程的返回值。

TCP/IP协议簇

IP层作用
IP头部占20字节
数据传送:将数据从一个主机传输到另一个主机。
寻址:根据子网划分和IP地址,发现正确的目的主机地址;
路由选择:选择数据在互联网上的传送路径;
数据报文的分段:当传输数据大于MTU时,将数据进行分段发送和接受并组装
TCP(传输控制协议)
头部占20字节
ACK的值是所接收到的SYN的值加1;
主机字节序:小端字节序(低位字节存储在低位地址)
网络字节序:大端字节序(高位字节存储在低位地址)
TCP网络编程
基础
1、创建套接字
套接字数据结构

struct sockaddr{                              //套接字地址结构
      sa_family_t sa_family;                 //协议簇
      char sa_data[4]                         //套接字协议数据(地址、端口和IP地址的信息)
      }
int bind(int sockfd,                          //套接字文件描述符
    const struct sockaddr *my_addr,           //套接字地址结构
    socklen_t addrlen);                       //地址结构长度

应用层函数socket()和内核函数之间的关系:用户调用sock=socket(AF_INET,SOCK_STREAM,0),这个函数会调用系统调用函数sys_socket(AF_INET,SOCK_STREAM,0)。系统调用sys_socket()分为两部分:①生成内核socket结构;②与文件描述符绑定,将绑定的文件描述符值传给应用层。
2、用bind()函数绑定地址端口

int bind(int sockfd,            //socket()创建的文件描述符
const struct sockaddr *my_addr, //包含IP地址、端口和IP地址的信息
socklen_t addrlen);             //地址长度,可设置为sizeof(struct sockaddr);

3、监听本地端口listen()

int listen(int sockfd,int backlog);    //backlog表示等待队列的长度

注:sockfd为监听描述符(作为客户端连接请求的一个端点;典型地,它被创建一次,并存在于服务器的整个生命周期);
listen()运行成功时返回0;失败返回-1;
4、接受请求accept()函数

扫描二维码关注公众号,回复: 5916149 查看本文章
int accept(int sockfd,struct sockaddra *addr,socklen_t *addrlen);

accept()函数的返回值是新连接的客户端套接字文件描述符,与客户端之间的通信是通过accept()函数返回的新套接字文件描述符进行的,而不是通过建立套接字时文件描述符。
5、连接目标网络服务器的connect()函数

int connect(int sockfd,struct sockaddr *,int addelen);

注:sockfd为已连接描述符(客户端和服务器之间已经建立起来了的连接的一个端点。服务器每次建立连接请求时,都会创建一次,只从在于服务器为一个客户端服务的过程中);
6、写入数据函数write()

int size;
char data[1024];
size=write(s,data,1024);

将缓存区data的数据写入到套接字文件描述符s中,返回值为写入成功的数据长度。
7、读取数据函数read()

int size;
char data[1024];
size=read(s,data,1024);

从套接字文件描述符s中读取1024个字节存放到缓冲区data中,size为成功读取的数据大小。
8、关闭套接字函数

int shutdown(int fd,int how);
参数s是切断套接口文件描述符,参数how设置实现方式。
SHUT_RD:值为0,表示切断读,之后不能使用次文件描述符进行读操作。
SHUT_WR:值为1,表示切断写,之后不能使用次文件描述符进行写操作。
SHUT_RDWR:值为2,表示切断读写,之后不能使用次文件描述符进行读写操作。与close()函数功能相同。

三次握手中如果第三次握手客户端发送的ACK报文丢失,会发生什么?
答:连接仍能正常工作,因为客户端已经处于estabilsh状态了,所以本地应用可以向另一方发送数据。每个报文段都有自己的ACK标志置位,而且ACK字段包含正确的数值,所以当第一个报文到达服务器时,他就会转换成establish即每个报文报告发送方希望看到的下一个序列号,即使这个序列号与之前一个或者多个序列号重复。
一方向另一发发送报文的同时也调用一个重传机制,如果没有出现期望的响应,最终会引起重发这个报文段。如果在几次重发后任然没有得到期望的响应,TCP就会放弃重传并回到close状态。
滑动窗口的作用:1、保证数据可靠和有序的传输;2、增强发送方和接收方之间的流量控制;
思想是不是发送发发送的数据超过接收方缓冲区的限度;
流量控制:防止发送方发出的数据超出了接收方接收数据的能力。
拥塞控制:防止过多的数据注入到网络而造成交换机或者链路超载。
服务器建立连接的主循环过程
在主循环中为了方便处理,每个客户端的连接请求服务器都会分叉一个进程进行处理。函数fork()出来的进程继承了父进程的属性,例如套接字描述符,在子进程和父进程中都有一套。
为了防止误操作,在父进程中关闭了客户端的套接字描述符,在子进程中关闭了父进程的侦听套接字描述符。一个进程的套接字文件描述符的关闭,不会造成套接字的真正关闭,因为仍然有一个进程在使用这些套接字描述符,只有所有的进程都关闭了这些描述符,Liunx才会释放他们。

**字节序转换函数介绍**
uint32_t htonl(uint32_t hostlong); 
**主机字节序到网络字节序的长整型转换**
uint16_t htons(uint16_t hostshort); 
**主机字节序到网络字节序的短整型转换**
uint32_t ntohl(uint32_t hostlong); 
**网络字节序到主机字节序的长整型转换**
uint16_t ntohs(uint16_t hostshort); 
/*网络字节序到主机字节序的短整型转换*/
函数命名规则:“字节序”to“字节序”“变量类型”。上述函数中h表示host,即主机字节序;n表示network,即网络字节序;l表示long型变量,s表示short变量。

数据的IO和复用
1、函数read()/write()和readv()/writev()可以对所有的文件,描述符使用;recv()/send()、recvfrom()/writeto()和recvmsg()/sendmsg()只能操作套接字描述符;
2、函数readv()/writev()和recvmsg()/sendmsg()可以操作多个缓冲区,read()/write()、recv()/send()和recvfrom()/sendto()只能操作单个缓冲区;
3、函数recv()/send()、recvfrom()/sendto()和recvmsg()/sendmsg()具有可选标志;
4、函数recvfrom()/sendto()和recvmsg()/sendmsg()可以选择对方的IP地址;
5、函数recvmsg()/sendmsg()有可选择的控制信息,能进行高级操作。
IO模型
1、阻塞IO模型:在数据接手时,数据没有到之前程序会一直等待;
2、非阻塞IO模型:当把套接字设置为非阻塞IO,则对每次请求,内核都不会阻塞,会立即返回;当没有数据的时候,会返回一个错误。
3、IO复用
使用IO复用模型可以在等待的时候加入超时的时间,当超时的时间没有到达的时候与阻塞的情况一致,而当超时时间到达任然没有数据接收到,系统会返回,不再等待。select()函数按照一定的超时时间轮询,知道需要等待的套接字有数据到来,利用recvfrom()函数将数据复制到应用层。
select()函数和pselect()函数用于IO复用,它们监视多个文件描述符的集合,判断是否有符合条件的事件发生。当一个或者多个监视的文件描述符准备就绪,可以进行IO操作的时候返回。
pselect()和select()区别:
(1)、pselect()超时的时间解耦是一个纳秒级的,而select()超时的时间结构是一个微秒级的;
(2)、pselect()增加了进入时替换掉的信号处理方式,当sigmask为NULL时,与select()的方式一样。
(3)、select()在执行之后可能会改变timeout的值,修改为还有多少时间剩余,而pselect()函数不会修改该值;
poll()和ppoll()函数
poll()函数等待某个文件描述符上的某个事件的发生。
poll()函数和ppoll()函数的区别如下:
(1)、poll()的超时时间timeout采用的是毫秒级的变量,而ppoll()函数为纳秒级的变量;
(2)、可以在ppoll()函数的处理过程中挂接临时的信号掩码。
4、信号驱动IO模型
信号驱动的IO在进程开始的时候注册一个信号处理的回调函数,进程继续执行,当信号发生时,即有了IO的时间,这里就有数据到来,利用注册的回调函数将到来的数据用recvfrom()函数接收到。
5、异步IO模型
异步IO与前面的信号驱动IO相似,其区别在于信号驱动IO当数据到来的时候,使用信号通知注册的信号处理函数,而异步IO则在数据复制完成的时候才发送信号通知注册的信号处理函数。
非阻塞方式的操作与阻塞方式的操作最大的不同点是函数的调用立即返回,不管数据是否成功读取或者成功写入。使用fcntl()函数将套接字文件描述符按照如下的代码进行设置后,可以进行非阻塞的编程。

funcl(s,F_SETFL,O_NONBLOCK);

其中s是套接字文件描述符,使用F_SETFL命令将套接字s设置为非阻塞方式后,再进行读写操作就可以马上返回了。
注意:使用轮询的方式进行查询十分浪费CPU等资源,不是十分必要,最好不要采用此种方法进行程序设计。
UDP编程框架
服务器端
(1)、建立套接字文件描述符,使用函数socket(),生成套接字文件描述符,例

int a=socket(AF_INET,SOCK_DGRAM,0);

(2)、设置服务器地址和侦听端口,初始化要绑定的网络地址结构,例

struct sockaddr addr_serv;
addr_serv.sin_family=AF_INTE;
addr_serv.sin_addr.s_addr=htonl(INADDR_ANY);   //任意本地接口
addr_serv.sin_port=htons(PORT_SERV);
注意sin_addr.s_addr和sin_port均为网络字节序。

(3)、绑定侦听端口,使用bind()函数,将套接字文件描述符和一个地质类型变量进行绑定,例

bind(s,(struct sockaddr*)&addr_serv,sizeof(addr_serv));

(4)、接收客户端的数据,使用recvfrom()函数接收客户端的网络数据;
(5)、向客户端发送数据,使用sendto()函数向服务器主机发送数据。
(6)、关闭套接字,使用close()函数释放资源。
客户端端
与服务器端相比,少了bind()部分,客户端程序的端口和本地的地址可以由系统在使用时指定。在使用sendto()和recvfrom()的时候,网络协议栈会临时指定本地的端口和地址。

(1)、建立套接字文件描述符,socket();
(2)、设置服务器端口和地址,struct sockaddr;
(3)、向服务器发送数据,sendto();
(4)、接收服务器的数据,recvfrom();
(5)、关闭套接字,close();

应用层recv()函数和内核函数的关系
应用层的recvfrom()对应内核层的sys_recvfrom()系统调用函数。系统调用函数sys_recvfrom()主要查找文件描述符对应的内核socket结构;建立一个消息结构;将用户空间的地址缓冲区指针和数据缓冲区指针打包到消息结构中;在套接字文件描述符中对应的数据链中查找对应的数据;将数据复制到消息中;销毁数据链中的数据;将数据复制到应用层空间中;减少文件描述符的引用计数。
sys_recvfrom()调用函数sockfd_lookup_light()查找到文件描述符对应的内核socket结构后,会申请一块内存用于保存连接成功的客户端的状态。socket结构的一些参数,例如类型type、操作方式ops等会继承服务器原来的值,如果原来服务器的类型为AF_INET,则其内核操作方式是af_inet.c文件中的各个函数。然后查找文件描述符表,获得一个新结构对应的文件描述符。
应用层sendto()函数和内核函数的关系
应用层的sendto()对应内核层的sys_sendto()系统调用函数。系统调用函数sys_sendto()主要查找文件描述符对应的内核socket结构;建立一个消息结构;将用户空间的地址缓冲区指针和数据缓冲区指针打包到消息结构中;在套接字文件描述符中对应的数据链中查找对应的数据;将数据复制到消息中;更新路由器信息;将数据复制到IP层;减少文件描述符的引用计数。
sys_sendto()调用函数sockfd_lookup_light()查找到文件描述符对应的内核socket结构后,会申请一块内存用于保存连接成功的客户端的状态。socket结构的一些参数,例如类型type、操作方式ops等会继承服务器原来的值,如果原来服务器的类型为AF_INET,则其内核操作方式是af_inet.c文件中的各个函数。然后查找文件描述符表,获得一个新结构对应的文件描述符。
UDP协议程序设计中的几大问题
1、UDP报文丢失数据
(1)、在Internet上,由于要经过多个路由器,正常情况下一个数据报文从主机C经过路由器A、路由器B、路由器C到达主机S。主机C使用sendto()发送数据,主机S使用recvfrom()接受数据,主机S在没有数据到来之前,会一直阻塞等待。
(2)、当UDP的数据报文丢失的时候,函数recvfrom()会一直阻塞,直到数据到来。正常情况下这种现象是不被允许的,可以设置超时时间来判断是否有数据到来;如果数据报文在经过路由器的时候,被路由器丢弃,则两个主机会对超时的数据进行重发。
2、UDP数据发送中的乱序
对策:采用发送端在数据段中加入数据报序号的方法,这样接受端对接收到数据的头端进行简单地处理就可以重新获得原始顺序的数据。
3、UDP协议中的connect()函数
connect()函数在TCP协议中会发生三次握手,建立一个持续的连接,一般不用于UDP。在UDP协议中connect()函数的作用仅仅表示确定了另一方的地址,并没有其他的含义。
connect()函数会产生如下的副作用:
(1)、使用connect()函数绑定套接字后,发送操作不能再使用sendto()函数,要使用write()函数直接操作套接字文件描述符,不在指定目的地址和端口。
(2)、使用connect()函数绑定套接字后,接收操作不能再次使用recvfrom()函数,要使用read()类的函数,函数发送方的地址和端口号。
(3)、在使用多次connect()函数的时候,会改变原来套接字绑定的目的地址和端口号,用新绑定的地址和端口号来代替,原有的绑定状态会失效。可以使用这种特点来断开原有的连接。
4、UDP缺乏流量控制
描述:UDP的接收缓冲区为多个缓冲区构成的一个环装数据缓冲区,起点为0;当接收到数据后,会将数据依次放入之前的数据的后面,并逐步增加缓冲区的序号。当数据没有接收或者接收数据比发送数据的速率要慢,之前接收的数据被覆盖,造成数据的丢失。
对策:根据实际情况采取增大接收数据缓冲区和接收方单独处理的方法来解决局部的UDP数据接收缓冲区溢出问题。
5、UDP协议中的数据报文截断
描述:当使用UDP协议接收数据的时候,如果应用程序传入的数据缓冲区的大小小于到来数据的大小,接收缓冲区会保存最大可能接收到的数据,其他的数据将会丢失,并且有MSG_TRUNC标志。
对策:服务器和客户端的程序要相互配合,接受的缓冲区要比发送的数据大一些,防止数据丢失的现象发生。
套接字选项
获取和设置套接字选项getsockopt()/setsockopt()
作用:获取和设置地址复用、读写数据的超时时间、对读缓冲区的大小进行调整等操作。

int getsockopt(int s,int level,int optname,void *optval,socklen_t *optlen);
int setsockopt(int s,int level,int optname,
const void *optval,socklen_t *optlen);

level表示选项所在协议层,当设置为SOL_SOCKET,用于获取或者设置通用的一些参数;当设置为IPPROTO_IP时,用于设置或者获取IP层的参数;当设置为IPPROTO_TCP,用于获取或者设置TCP层的一些参数。optname为选项名。optval为操作的内存缓冲区,对于getsockopt()指向返回选项值的缓冲区,对于getsockopt()指向设置的缓冲区。optlen问第四个参数的长度。
Nagle算法
思想:只要TCP发出了数据,发送方终究会收到一个ACK。可以把这个ACK看成一个激活的定时器,触发传输更多的数据。
原理:(1)、将小分组包装为更大的帧进行发送。Nagle算法总是最大可能的发送最大分组,将小分组包装,小分组是指小于MSS的分组。在之前的数据被确认之前不再发送数据分组,即Magle算法需要之前数据接收方的响应。
(2)、Nagle算法通常在接收端使用延迟确认,在接收到数据后并不马上发送确认,而是要等待一小段时间。这样可以与接收方的有效数据一起发送ACK,即接收方向发送方发送的确认分组中包含发送给发送方的有效载荷数据。
ioctl()函数:liunx下与内核进行交互的一种方法,在网络程序设计中,广泛使用ioctl()函数与内核中的网络协议栈进行交互。
Nagle算法伪代码

if there is new data to send
  if the window size >= MSS and available data is >= MSS
        send complete MSS segment now
  else
    if there is unconfirmed data still in the pipe
        enqueue data in the buffer until an acknowledge is received
    else
        send data immediately
    end if
  end if
end if

这段代码的意思是如果要发送的数据大于MSS的话,立即发送。否则:看看前面发出去的包是不是还有没有ack的,如果有没有ack的那么我这个小包不急着发送,等前面的ack回来再发送。
Nagle算法逻辑就是:如果发送的包很小(不足MSS),又有包发给了对方对方还没回复说收到了,那我也不急着发,等前面的包回复收到了再发。这样可以优化带宽利用率(早些年带宽资源还是很宝贵的),Nagle算法也是用来优化改进tcp传输效率的。
自适应超时重传机制:P237
延迟确认Delay Ack
TCP是可靠传输,可靠的核心是收到包后回复一个ack来告诉对方收到了。
delay ack是指收到包后不立即ack,而是等一小会(比如40毫秒)看看,如果这40毫秒以内正好有一个包发给client,那么我这个ack包就跟着发过去,这样节省了资源。当然如果超过这个时间还没有包发给client,那么这个ack也要发给client了(。
假如这个时候ack包还在等待延迟发送的时候,又收到了client的一个包,那么这个时候server有两个ack包要回复,那么os会把这两个ack包合起来立即回复一个ack包给client,告诉client前两个包都收到了。
也就是delay ack开启的情况下:ack不立即发而是等40毫秒,等的过程中ack包有顺风车就搭;或者如果凑够两个ack包自己包个车也立即发车;再如果等了40毫秒以上也没顺风车,那么自己打个车也发车。
记录边界
机制1:使用紧急数据特;
机制2:向字节流插入记录结束标记的机制是push操作;
资源分配:在一个过程中,网络设备尽量满足应用对网络资源的竞争需求,这里的资源主要是指链路带宽和路由器或者交换机上的缓冲区空间。
int ioctl(int d,int request,…);
主要包含对套接字、文件、网络接口、地址解析协议(ARP)和路由等的操作请求。
fcntl()函数:对套接字描述符进行操作,同样可以对通用文件描述符进进行操作。
int fcntl(int fd,int cmd,void arg);
phread_create()和pthread_join()函数
phread_create()函数是UNIX环境创建线程函数:
头文件 :

#include <pthread.h>
int pthread_create(pthread_t *restrict tidp,const pthread_attr_t *restrict_attr,void*(*start_rtn)(void*),void *restrict arg);

返回值:若成功则返回0,否则返回出错编号
参数:第一个参数为指向线程标识符的指针;第二个参数用来设置线程属性;第三个参数是线程运行函数的地址;最后一个参数是运行函数的参数。
注意:在编译时注意加上-lpthread参数,以调用静态链接库。因为pthread并非Linux系统的默认库。
pthread_join()函数用来等待一个线程的结束,线程间同步的操作。
头文件 :

#include <pthread.h>
int pthread_join(pthread_t thread, void **retval);

描述 :pthread_join()函数,以阻塞的方式等待thread指定的线程结束。当函数返回时,被等待线程的资源被收回。如果线程已经结束,那么该函数会立即返回。并且thread指定的线程必须是joinable的。
参数 :thread: 线程标识符,即线程ID,标识唯一线程。
retval: 用户定义的指针,用来存储被等待线程的返回值。
返回值 :0代表成功。 失败,返回的则是错误号。
pthread_exit():终止当前线程
void pthread_exit(void* retval);
linux下,用pthread_create创建线程后,线程的默认状态为joinable,如果程序退出,线程没被join,则会有线程的资源没有被释放。
调用pthread_join可以,但是程序会再调用该函数后阻塞住。
替代的调用pthread_detach,该函数可立即返回,有2种方法。
1:子线程里调用:pthrad_detach(pthread_self());
2:父线程里调用:pthread_detach(thread_id);
调用之后,子进程的资源控制权就交还给父线程了,这样线程退出就不会出现资源泄漏了。
IO复用的核心程序的主要过程
(1)、初始化select()需要的文件描述符、写文件描述符;
(2)、将放置文件描述符的数组connect_host[]中的合法的文件描述符放入select()的文件描述符集合中,这里同时更新了select需要设置的MAXFD,设置文件描述符完毕后,maxfd参数中放置的为当前最大的文件描述符;
(3)、然后进行select,等待IO复用可用条件的满足:数据到来或者超时;
(4)、在select结束的时候,先检查select返回值的合法性;
(5)、在select合法的时候,根据connect_host[]中的文件描述符判定是否此文件描述符的数据到来;
(6)、对于激活的文件描述符,从文件描述符中接收数据、判断接收的数据,并向客户端返回消息;
(7)、对于处理完毕的文件描述符设置在connect_host[]中的标记为-1,表明此文件描述符已经处理过了,不在有效,然后关闭客户端连接;

并发编程

1、基于进程的并发编程
(1)、共享文件变,但是不共享地址空间,有独立的地址空间,既是优点也是缺点;
(2)、独立的地址空间使得进程共享状态信息变得困难,为了共享信息,需要使用IPC(进程间通信)机制;
(3)、比较慢,进程控制和IPC的开销很高;
2、IO多路复用的并发编程
使用select函数,要求内核挂起进程,只有在一个或者多个IO事件发生时,才将控制返回给应用程序;
select函数有两个输入:一个称为读集合的描述符集合fdset和该读集合的元素数量n。select函数会一直阻塞,直到读集合中至少有一个描述符准备好可以读。当且仅当一个从该描述符读取一个字节的请求不会阻塞时,描述符k就表述准备可以读了。作为一个副作用,select修改了fdset指向的fd_set,指明读集合中一个称为准备好集合的子集,这个集合是由读集合中准备好可以读了的描述符组成。函数返回的值指明了准备好集合的元素量。注意,由于这个副作用,我们必须在每次调用select是都更新读集合。
socket通信中select函数的使用和解释
select函数的作用:
select()在SOCKET编程中还是比较重要的,可是对于初学SOCKET的人来说都不太爱用select()写程序,他们只是习惯写诸如 conncet()、accept()、recv()或recvfrom这样的阻塞程序(所谓阻塞方式block,顾名思义,就是进程或是线程执行到这些函数时必须等待某个事件发生,如果事件没有发生,进程或线程就被阻塞,函数不能立即返回)。可是使用select()就可以完成非阻塞(所谓非阻塞方式non-block,就是进程或线程执行此函数时不必非要等待事件的发生,一旦执行肯定返回,以返回值的不同来反映函数的执行情况。如果事件发生则与阻塞方式相同,若事件没有发生则返回一个代码来告知事件未发生,而进程或线程继续执行,所以效率高)方式工作的程序,它能够监视我们需要监视的文件描述符的变化情况——读写或是异常。
select函数格式:
select()函数的格式(所说的是Unix系统下的Berkeley Socket编程,和Windows下的有区别,一会儿说明):
Unix系统下解释:
int select(int maxfdp, fd_set* readfds, fd_set* writefds, fd_set* errorfds, struct timeval* timeout);
先说明两个结构体:
第一:struct fd_set可以理解为一个集合,这个集合中存放的是文件描述符(file descriptor),即文件句柄,这可以是我们所说的普通意义的文件,当然Unix下任何设备、管道、FIFO等都是文件形式,全部包括在内,所以,毫无疑问,一个socket就是一个文件,socket句柄就是一个文件描述符。fd_set集合可以通过一些宏由人为来操作,比如清空集合:FD_ZERO(fd_set*),将一个给定的文件描述符加入集合之中FD_SET(int, fd_set*),将一个给定的文件描述符从集合中删除FD_CLR(int, fd_set*),检查集合中指定的文件描述符是否可以读写FD_ISSET(int, fd_set*)。一会儿举例说明。
第二:struct timeval是一个大家常用的结构,用来代表时间值,有两个成员,一个是秒数,另一个毫秒数。
具体解释select的参数:int maxfdp是一个整数值,是指集合中所有文件描述符的范围,即所有文件描述符的最大值加1,不能错!在Windows中这个参数值无所谓,可以设置不正确。
fd_set* readfds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的读变化的,即我们关心是否可以从这些文件中读取数据了,如果这个集合中有一个文件可读,select就会返回一个大于0的值,表示有文件可读,如果没有可读的文件,则根据timeout参数再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的读变化。
fd_set* writefds是指向fd_set结构的指针,这个集合中应该包括文件描述符,我们是要监视这些文件描述符的写变化的,即我们关心是否可以向这些文件中写入数据了,如果这个集合中有一个文件可写,select就会返回一个大于0的值,表示有文件可写,如果没有可写的文件,则根据timeout再判断是否超时,若超出timeout的时间,select返回0,若发生错误返回负值。可以传入NULL值,表示不关心任何文件的写变化。
fe_set* errorfds同上面两个参数的意图,用来监视文件错误异常。
struct timeval* timeout是select的超时时间,这个参数至关重要,它可以使select处于三种状态。
第一:若将NULL以形参传入,即不传入时间结构,就是将select置于阻塞状态,一定等到监视文件描述符集合中某个文件描述符发生变化为止;
第二:若将时间值设为0秒0毫秒,就变成一个纯粹的非阻塞函数,不管文件描述符是否有变化,都立刻返回继续执行,文件无变化返回0,有变化返回一个正值;
第三:timeout的值大于0,这就是等待的超时时间,即select在timeout时间内阻塞,超时时间之内有事件到来就返回了,否则在超时后不管怎样一定返回,返回值同上述。
select函数返回值:
负值:select错误
正值:某些文件可读写或出错
0:等待超时,没有可读写或错误的文件
Windows平台下解释:
1,函数原型:
int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, const struct timeval* timeout);
2,参数:
nfds: 本参数忽略,仅起到兼容作用,设为0即可;
readfds: (可选)指针,指向一组等待可读性检查的套接口;
writefds: (可选)指针,指向一组等待可写性检查的套接口;
exceptfds:(可选)指针,指向一组等待错误检查的套接口;
timeout: 本函数最多等待时间,对阻塞操作则为NULL。
3,返回值:
(1)select()调用返回处于就绪状态并且已经包含在fd_set结构中的描述字总数;
(2)如果超时则返回0;
(3)否则的话,返回SOCKET_ERROR错误,应用程序可通过WSAGetLastError()获取相应错误代码。
4,注释:
本函数用于确定一个或多个套接口的状态。对每一个套接口,调用者可查询它的可读性、可写性及错误状态信息。用fd_set结构来表示一组等待检查的套接口。在调用返回时,这个结构存有满足一定条件的套接口组的子集,并且select()返回满足条件的套接口的数目。有一组宏可用于对fd_set的操作,这些宏与Berkeley Unix软件中的兼容,但内部的表达是完全不同的。
readfds参数标识等待可读性检查的套接口。如果该套接口正处于监听listen()状态,则若有连接请求到达,该套接口便被标识为可读,这样一个accept()调用保证可以无阻塞完成。对其他套接口而言,可读性意味着有排队数据供读取。或者对于SOCK_STREAM类型套接口来说,相对于该套接口的虚套接口已关闭,于是recv()或recvfrom()操作均能无阻塞完成。如果虚电路被“优雅地”中止,则recv()不读取数据立即返回;如果虚电路被强制复位,则recv()将以WSAECONNRESET错误立即返回。如果SO_OOBINLINE选项被设置,则将检查带外数据是否存在(参见setsockopt())。
writefds参数标识等待可写性检查的套接口。如果一个套接口正在connect()连接(非阻塞),可写性意味着连接顺利建立。如果套接口并未处于connect()调用中,可写性意味着send()和sendto()调用将无阻塞完成。〔但并未指出这个保证在多长时间内有效,特别是在多线程环境中〕。
exceptfds参数标识等待带外数据存在性或意味错误条件检查的套接口。请注意如果设置了SO_OOBINLINE选项为假FALSE,则只能用这种方法来检查带外数据的存在与否。对于SO_STREAM类型套接口,远端造成的连接中止和KEEPALIVE错误都将被作为意味出错。如果套接口正在进行连接connect()(非阻塞方式),则连接试图的失败将会表现在exceptfds参数中。
如果对readfds、writefds或exceptfds中任一个组类不感兴趣,可将它置为空NULL。
在winsock2.h头文件中共定义了四个宏来操作描述字集。FD_SETSIZE变量用于确定一个集合中最多有多少描述字(FD_SETSIZE缺省值为64,可在包含winsock.h前用#define FD_SETSIZE来改变该值)。对于内部表示,fd_set被表示成一个套接口的队列,最后一个有效元素的后续元素为INVAL_SOCKET。宏为:
FD_CLR(s,*set): 从集合set中删除描述字s。 FD_ISSET(s,*set): 若s为集合中一员,非零;否则为零。 FD_SET(s,*set): 向集合添加描述字s。 FD_ZERO(*set): 将set初始化为空集NULL。 timeout参数控制select()完成的时间。若timeout参数为空指针,
则select()将一直阻塞到有一个描述字满足条件。否则的话,timeout指向一个timeval结构,其中指定了select()调用在返回前等待多长时间。如果timeval为{0,0},则select()立即返回,这可用于探询所选套接口的状态。如果处于这种状态,则select()调用可认为是非阻塞的,且一切适用于非阻塞调用的假设都适用于它。
5,错误代码:

    WSANOTINITIALISED:在使用此API之前应首先成功地调用WSAStartup()。
    WSAENETDOWN:      WINDOWS套接口实现检测到网络子系统失效。
    WSAEINVAL:        超时时间值非法。
    WSAEINTR:         通过一个WSACancelBlockingCall()来取消一个(阻塞的)调用。
    WSAEINPROGRESS:   一个阻塞的WINDOWS套接口调用正在运行中。
    WSAENOTSOCK:      描述字集合中包含有非套接口的元素。

6,如何处理
上面在说明FD_SETSIZE时,winsock2.h中定义``FD_SETSIZE的大小为64,这样就对readfds、writefds、exceptfds的socket句柄数进行了限制。在实际应用中可以使用端口分组或者重新定义FD_SETSIZE```的方式进行解决。在stdAfx.h最末行添加如下定义:

#define FD_SETSIZE 1024                  //socket句柄数
#define MAXIMUM_WAIT_OBJECTS    1024     //要等待的对象数
要注意的是我们还重定义了要另一个宏MAXIMUM_WAIT_OBJECTS,它表示要等待的对象数。重定义后,程序在现场运行正常。

epoll函数
epoll使用一组函数来完成任务,而不是使用一个函数。epoll把用户关心的文件描述符中的时间放在了内核中的一个事件表表中,而无需向select和poll那样每次调用都要重复传入事件描述符集或者事件集。但epoll使用一个额外的文件描述符来唯一标识内核中的这个事件。
epoll系列系统调用的主要接口是epoll_wait函数,,它在一段超时时间内等待一组文件描述符上的事件。成功时返回就绪文件描述符的个数。
Linux Sendfile 的优势
Sendfile 函数在两个文件描述符之间直接传递数据(全然在内核中操作,传送),从而避免了内核缓冲区数据和用户缓冲区数据之间的拷贝,操作效率非常高,被称之为零拷贝。
Sendfile 函数的定义例如以下:

#include<sys/sendfile.h>
ssize_t sendfile(int out_fd,int in_fd,off_t*offset,size_t count);

传统方式read/write send/recv
在传统的文件传输里面(read/write方式),在实现上事实上是比較复杂的,须要经过多次上下文的切换。我们看一下例如以下两行代码:

1. read(file, tmp_buf, len);       
2. write(socket, tmp_buf, len);  

以上两行代码是传统的read/write方式进行文件到socket的传输。
当须要对一个文件进行传输的时候,其详细流程细节例如以下:
1、调用read函数,文件数据被copy到内核缓冲区
2、read函数返回。文件数据从内核缓冲区copy到用户缓冲区
3、write函数调用。将文件数据从用户缓冲区copy到内核与socket相关的缓冲区。
4、数据从socket缓冲区copy到相关协议引擎。
以上细节是传统read/write方式进行网络文件传输的方式,我们能够看到,在这个过程其中。文件数据实际上是经过了四次copy操作:
硬盘—>内核buf—>用户buf—>socket相关缓冲区(内核)—>协议引擎
新方式sendfile
而sendfile系统调用则提供了一种降低以上多次copy。提升文件传输性能的方法。
Sendfile系统调用是在2.1版本号内核时引进的:

  1. sendfile(socket, file, len);
    执行流程例如以下:
    1、sendfile系统调用,文件数据被copy至内核缓冲区
    2、再从内核缓冲区copy至内核中socket相关的缓冲区
    3、最后再socket相关的缓冲区copy到协议引擎
    相较传统read/write方式,2.1版本号内核引进的sendfile已经降低了内核缓冲区到user缓冲区。再由user缓冲区到socket相关 缓冲区的文件copy,而在内核版本号2.4之后,文件描写叙述符结果被改变,sendfile实现了更简单的方式,系统调用方式仍然一样,细节与2.1版本号的 不同之处在于,当文件数据被拷贝到内核缓冲区时,不再将全部数据copy到socket相关的缓冲区,而是只将记录数据位置和长度相关的数据保存到 socket相关的缓存,而实际数据将由DMA模块直接发送到协议引擎,再次降低了一次copy操作。
    mmap函数和munmap函数
    mmap函数用于申请一段内存空间。我们可以将这段内存作为进程间通信的共享内存,也可以将文件直接映射到其中。munmap函数则释放由mmap创建的这段内存空间。它们的定义如下:
#include <sys/mman.h>
void* mmap(void* start, size_t length, int prot, int flags, int fd, off_t offset);
int munmap(void* start, size_t length);

start参数允许用户使用某个特定的地址作为这段内存的起始地址。如果它被设置成NULL,则系统自动分配一个地址。length参数指定内存段的长度。prot参数用来设置内存段的访问权限。
零拷贝之splice( )函数和tee( )函数
splice( )函数
在两个文件描述符之间移动数据,同sendfile( )函数一样,也是零拷贝。
函数原型:

#include <fcntl.h>
ssize_t splice(int fdin, loff_t *offin, int fdout, loff_t *offout, size_t len, unsigned int flags);

tee( )函数
在两个管道文件描述符之间复制数据,同是零拷贝。但它不消耗数据,数据被操作之后,仍然可以用于后续操作。
函数原型:

#include <fcntl.h>
ssize_t tee(int fdin, int fdout, size_t len, unsigned int flags);

fcntl函数
fcntl系统调用可以用来对已打开的文件描述符进行各种控制操作以改变已打开文件的的各种属性

#include<unistd.h>  
#include<fcntl.h>  
int fcntl(int fd, int cmd);  
int fcntl(int fd, int cmd, long arg);  
int fcntl(int fd, int cmd ,struct flock* lock);  

close_wait
在被动关闭连接情况下,在已经接收到FIN,但是还没有发送自己的FIN的时刻,连接处于CLOSE_WAIT状态。
通常来讲,CLOSE_WAIT状态的持续时间应该很短,正如SYN_RCVD状态。但是在一些特殊情况下,就会出现连接长时间处于CLOSE_WAIT状态的情况。
出现大量close_wait的现象,主要原因是某种情况下对方关闭了socket链接,但是我方忙与读或者写,没有关闭连接。代码需要判断socket,一旦读到0,断开连接,read返回负,检查一下errno,如果不是AGAIN,就断开连接。
解决方法
基本的思想就是要检测出对方已经关闭的socket,然后关闭它。
1.代码需要判断socket,一旦read返回0,断开连接,read返回负,检查一下errno,如果不是AGAIN,也断开连接。(注:在UNP7.5节的图7.6中,可以看到使用select能够检测出对方发送了FIN,再根据这条规则就可以处理CLOSE_WAIT的连接)
2.给每一个socket设置一个时间戳last_update,每接收或者是发送成功数据,就用当前时间更新这个时间戳。定期检查所有的时间戳,如果时间戳与当前时间差值超过一定的阈值,就关闭这个socket。
3.使用一个Heart-Beat线程,定期向socket发送指定格式的心跳数据包,如果接收到对方的RST报文,说明对方已经关闭了socket,那么我们也关闭这个socket。
4.设置SO_KEEPALIVE选项,并修改内核参数。
time_wait
1、time_wait状态如何产生?
首先调用close()发起主动关闭的一方,在发送最后一个ACK之后会进入time_wait的状态,也就说该发送方会保持2MSL时间之后才会回到初始状态。MSL值得是数据包在网络中的最大生存时间。产生这种结果使得这个TCP连接在2MSL连接等待期间,定义这个连接的四元组(客户端IP地址和端口,服务端IP地址和端口号)不能被使用。
2.time_wait状态产生的原因
1)为实现TCP全双工连接的可靠释放
由TCP状态变迁图可知,假设发起主动关闭的一方(client)最后发送的ACK在网络中丢失,由于TCP协议的重传机制,执行被动关闭的一方(server)将会重发其FIN,在该FIN到达client之前,client必须维护这条连接状态,也就说这条TCP连接所对应的资源(client方的local_ip,local_port)不能被立即释放或重新分配,直到另一方重发的FIN达到之后,client重发ACK后,经过2MSL时间周期没有再收到另一方的FIN之后,该TCP连接才能恢复初始的CLOSED状态。如果主动关闭一方不维护这样一个TIME_WAIT状态,那么当被动关闭一方重发的FIN到达时,主动关闭一方的TCP传输层会用RST包响应对方,这会被对方认为是有错误发生,然而这事实上只是正常的关闭连接过程,并非异常。
2)为使旧的数据包在网络因过期而消失
为说明这个问题,我们先假设TCP协议中不存在TIME_WAIT状态的限制,再假设当前有一条TCP连接:(local_ip, local_port, remote_ip,remote_port),因某些原因,我们先关闭,接着很快以相同的四元组建立一条新连接。本文前面介绍过,TCP连接由四元组唯一标识,因此,在我们假设的情况中,TCP协议栈是无法区分前后两条TCP连接的不同的,在它看来,这根本就是同一条连接,中间先释放再建立的过程对其来说是“感知”不到的。这样就可能发生这样的情况:前一条TCP连接由local peer发送的数据到达remote peer后,会被该remot peer的TCP传输层当做当前TCP连接的正常数据接收并向上传递至应用层(而事实上,在我们假设的场景下,这些旧数据到达remote peer前,旧连接已断开且一条由相同四元组构成的新TCP连接已建立,因此,这些旧数据是不应该被向上传递至应用层的),从而引起数据错乱进而导致各种无法预知的诡异现象。作为一种可靠的传输协议,TCP必须在协议层面考虑并避免这种情况的发生,这正是TIME_WAIT状态存在的第2个原因。
3)总结
具体而言,local peer主动调用close后,此时的TCP连接进入TIME_WAIT状态,处于该状态下的TCP连接不能立即以同样的四元组建立新连接,即发起active close的那方占用的localport在TIME_WAIT期间不能再被重新分配。由于TIME_WAIT状态持续时间为2MSL,这样保证了旧TCP连接双工链路中的旧数据包均因过期(超过MSL)而消失,此后,就可以用相同的四元组建立一条新连接而不会发生前后两次连接数据错乱的情况。
3.time_wait状态如何避免
首先服务器可以设置SO_REUSEADDR套接字选项来通知内核,如果端口忙,但TCP连接位于TIME_WAIT状态时可以重用端口。在一个非常有用的场景就是,如果你的服务器程序停止后想立即重启,而新的套接字依旧希望使用同一端口,此时SO_REUSEADDR选项就可以避免TIME_WAIT状态。
原子操作
“原子操作是不需要synchronized”,,不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何线程切换。
volatile是一个类型描述符,要求编译器不要对其描述的对象作优化处理,对它的读写都需要从内存中访问。

操作系统

在同一个操作系统中,不同的进程经常需要相互协同工作,协同的方法一般有两种,一是直接共享逻辑地址空间,二是通过文件或消息共享数据。如果共享逻辑地址空间,则在进程执行的时候有可能会发生多个进程同时访问同一个数据的冲突问题,特别是在多处理器的情况下。对于这类冲突,内核采用了一些方法进行进程同步,例如原子操作、自旋锁、信号量等方法。
临界区
临界区(critical-section)是解决进程协作的一个方法。将多个进程可能修改同一个共享变量的代码段设为临界区,当有进程进入临界区后,其他进程会被禁止进入,直到前一个进程离开临界区,其他进程才可以进入。即同一时刻只允许一个进程位于临界区内。伪代码形式可以表示为:

do{
	//进入区
	//临界区
	//退出区
	//剩余区
}while(TRUE)

临界区的实现需要满足以下三个条件:
1、互斥,即同一时刻只能有一个进程位于临界区内;
2、前进,当多个进程同时等待进入临界区的时候,会有一个进程被选择进入;
3,有限等待,在进入区等待的进程必须在有限时间后进入临界区。
作为所有软件都可能会调用的操作系统,同一时刻内同一段代码可能会有多个进程在执行,而像文件读写、硬件调用等操作都是排他性的,因此操作系统更应该做好临界区的设置。
对于操作系统的临界区实现,要分为抢占内核和非抢占内核来讨论。显然,非抢占内核不存在竞争的问题,因为在临界区内的进程不会被打断,除非进程主动退出。对抢占内核来说,就需要硬件或者软件(算法)上的支持来实现临界区。
软件支持的一个例子是Peterson算法。Peterson算法的精髓在于用两个变量(或数组)来记录当前是否有进程位于临界区以及哪个进程位于临界区,这样通过在进入区检测并设置标记、退出区恢复标记可以实现临界区排他的特性。
硬件支持的方法是,从底层硬件的层面来看则是实现原子操作。进程在进入临界区前检测并申请锁,离开后释放锁。原子操作保证锁的正常运行。
自旋锁
自旋锁就是一种共享资源保护机制,确保同一时刻只有一个进程能访问到共享资源。
自旋锁的功能有两点,一是临界区代码任意时刻由一个CPU进行访问,二是当前CPU访问期间不会发生进程切换
优先队列(priority_queue)
普通的队列是一种先进先出的数据结构,元素在队列尾追加,而从队列头删除。在优先队列中,元素被赋予优先级。当访问元素时,具有最高优先级的元素最先删除。优先队列具有最高级先出 (first in, largest out)的行为特征。通常采用数据结构来实现。
数据库索引
索引是对数据库表中一列或多列的值进行排序的一种结构,使用索引可快速访问数据库表中的特定信息。
索引的一个主要目的就是加快检索表中数据的方法,亦即能协助信息搜索者尽快的找到符合限制条件的记录ID的辅助数据结构。
数据库事务
事务(Transaction)是由一系列对系统中数据进行访问与更新的操作所组成的一个程序执行逻辑单元。
事务具有4个基本特征,分别是:原子性(Atomicity)、一致性(Consistency)、隔离性(Isolation)、持久性(Duration),简称ACID。
4个隔离级别分别是:读未提及(READ_UNCOMMITTED)、读已提交(READ_COMMITTED)、可重复读(REPEATABLE_READ)、顺序读(SERIALIZABLE)。
内存池
(Memory Pool)是一种内存分配方式。通常我们习惯直接使用new、malloc等API申请分配内存,这样做的缺点在于:由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片并进而降低性能。
内存池则是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。这样做的一个显著优点是,使得内存分配效率得到提升。
进程就绪状态是指进程已获得除CPU之外的所有必须资源,只等待操作系统利用CPU调度算法将CPU分配给该进程以便执行. 进程阻塞状态也称进程等待状态,是指进程等待某一特定事件的出现(如I/O操作),在该过程中,进程依旧位于内存内,且占有CPU资源.

猜你喜欢

转载自blog.csdn.net/zhaoyinhui0802/article/details/89337961