Youmai の Blog


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

malloc和free的实现原理

发表于 2015-03-27 | 分类于 c

之前写过一篇为什么free函数不需要指定需要释放的内存大小,这个问题算是解释清楚了,但是针对malloc的实现又不怎么记得了,抽空又把《深入理解计算机系统》相关段落看了一遍,真的是好书啊!浅显易懂,真是适合我。


内存分配是按照堆块实现的,一个堆块是由头部和有效载荷量组成,其中的有效载荷量就是我们申请的堆的大小。
头部块包括 块大小和是否可用这两个部分组成。

在内存中这些堆块以链表形式组成

malloc函数的实质体现在:它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。

这里注意,malloc找到的内存块大小一定是会大于等于我们需要的内存大小,下面会提到如果所有的内存块都比要求的小会怎么办?

调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。

glibc维护了不止一个不定长的内存块链表,而是好几个,每一个这种链表负责一个大小范围,这种做法有效减少了分配大内存时的遍历开销,类似于哈希的方式,将很大的范围的数据散列到有限的几个小的范围内而不是所有数据都放在一起,虽然最终还是要在小的范围内查找,但是最起码省去了很多的开销,如果只有一个不定长链表那么就要全部遍历,如果分成3个,就省去了2/3的开销,总之这个策略十分类似于散列。

glibc另外的策略就是不止维护一类空闲链表,而是另外再维护一个缓冲链表和一个高速缓冲链表,在分配的时候首先在高速缓存中查找,失败之后再在空闲链表查找,如果找到的内存块比较大,那么将切割之后的剩余内存块插入到缓存链表,如果空闲链表查找失败那么就往缓存链表中查找. 如果还是没有合适的空闲块,就向内存申请比请求数更大的内存块,然后把剩下的内存放入链表中。

在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc返回的每块内存的起始处首先要有这个结构:

这就解释了,为什么在程序中free之后,但是堆的内存还是没有释放。

内存控制块结构定义
struct mem_control_block {
    int is_available;
    int size;
};

现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。

在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:

解除分配函数
void free(void *firstbyte) {
        struct mem_control_block *mcb;
        /* Backup from the given pointer to find the
         * mem_control_block
         */
       mcb = firstbyte - sizeof(struct mem_control_block);
        /* Mark the block as being available */
          mcb->is_available = 1;
        /* That''s It!  We''re done. */
       return;
}

fork和exec

发表于 2015-03-25 | 分类于 linux

##fork()
一个程序调用fork函数。首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。

事实上,目前大多数的unix系统在实现上并没有作真正的copy。一般的,CPU都是以“页”为单位分配空间的,象INTEL的CPU,其一页在通常情况下是4K字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。

##exec系列函数
一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。

##对于fork():

  1. 子进程复制父进程的所有进程内存到其内存地址空间中。父、子进程的
    “数据段”,“堆栈段”和“代码段”完全相同,即子进程中的每一个字节都
    和父进程一样。
  2. 子进程的当前工作目录、umask掩码值和父进程相同,fork()之前父进程
    打开的文件描述符,在子进程中同样打开,并且都指向相同的文件表项。
  3. 子进程拥有自己的进程ID。

##对于exec():

  1. 进程调用exec()后,将在同一块进程内存里用一个新程序来代替调用
    exec()的那个进程,新程序代替当前进程映像,当前进程的“数据段”,
    “堆栈段”和“代码段”被新程序改写。
  2. 新程序会保持调用exec()进程的ID不变。
  3. 调用exec()之前打开打开的描述字继续打开(好像有什么参数可以令打开
    的描述字在新程序中关闭)

TCP是一种流协议

发表于 2015-03-24 | 分类于 TCP/IP

之前写过一篇是tcp是流的一些思考–拆包和粘包,是我自己对这个问题的一些认识,毕竟见识浅薄,所言不成系统。今天看到了之前看过的一篇文章,正是对这个问题的一些深入解释,特转载于此,与大家共享。

转载自《TCP/IP高效编程 改善网络程序的44个技巧》–技巧6:记住TCP是一种流协议


TCP是一种流协议(stream protocol)。这就意味着数据是以字节流的形式传递给接收者的,没有固有的”报文”或”报文边界”的概念。从这方面来说,读取TCP数据就像从串行端口读取数据一样–无法预先得知在一次指定的读调用中会返回多少字节。

为了说明这一点,我们假设在主机A和主机B的应用程序之间有一条TCP连接,主机A上的应用程序向主机B发送一条报文。进一步假设主机A有两条报文要发送,并两次调用send来发送,每条报文调用一次。很自然就会想到从主机A向主机B发送的两条报文是作为两个独立实体,在各自的分组中发送的,如图2-25所示。

图2-25 发送两条报文的错误模型

但不幸的是,实际的数据传输过程很可能不会遵循这个模型。主机A上的应用程序会调用send,我们假设这条写操作的数据被封装在一个分组中传送给B。实际上,send通常只是将数据复制到主机A的TCP/IP栈中,就返回了。由TCP来决定(如果有的话)需要立即发送多少数据。做这种决定的过程很复杂,取决于很多因素,比如发送窗口(当时主机B能够接收的数据量),拥塞窗口(对网络拥塞的估计),路径上的最大传输单元(沿着主机A和B之间的网络路径一次可以传输的最大数据量),以及连接的输出队列中有多少数据。更多与此有关的内容请参见技巧15。图2-26只显示了主机A的TCP封装数据时可能使用的诸多方法中的4种。在图2-26中,M11和M12表示M1的第一和第二部分,M21和M22与之类似。如图2-26所示,TCP不一定会将一条报文的全部内容都放在一个分组中传送出去。

图2-26 封装两条报文可能采用的4种方式

现在,我们从主机B应用程序的角度来看这种情形。总的来说,主机B应用程序任意一次调用recv时,都不会对TCP发送给它的数据量做任何假设。比如,当主机B应用程序读取第一条报文时,可能会出现下列4种结果。

 实际上,可能的结果不止4种,但我们忽略了出错和EOF之类的结果。我们还假设应用程序读取了所有可读的数据。

  1. 没有数据可读,应用程序阻塞,或者recv返回一条指示说明没有数据可读。到底会发生什么情况取决于套接字是否标识为阻塞,以及主机B的操作系统为系统调用recv指定了什么样的语义。

  2. 应用程序获取了报文M1中的部分而不是全部数据。比如,发送端TCP像图2-26D那样对数据进行分组就会发生这种情况。

  3. 应用程序获取了报文M1中所有的数据,除此之外没有任何其他内容。如果像图2-26A那样对数据分组就会发生这种情况。

  4. 应用程序获取了报文M1的所有数据,以及报文M2的部分或全部数据。如果像图2-26B或图2-26C那样对数据进行分组就会发生这种情况。

注意,这里还有一个定时问题。如果主机B的应用程序在主机A发送了第二条报文之后一段时间内都没有读取第一条报文,那么这两条报文都会成为可读的。这就和图2-26B所示情况相同了。这些描述说明,通常,在任意指定时刻,可读的数据量都是不确定的。

需要再次说明的是,TCP是一个流协议(stream protocol),尽管数据是以IP分组的形式传输的,但分组中的数据量与send调用中传送给TCP多少数据并没有直接关系。而且,接收程序也没有什么可靠的方法可以判断数据是如何分组的,因为在两次recv调用之间可能会有多个分组到来。

即使接收端应用程序的响应非常及时,也可能会发生这种情况。例如,一个分组丢失了(参见技巧12,在当今的因特网中,这是非常常见的情况),而且后继分组都安全到达,TCP会将后继分组中的数据保存起来,直到重传第一个分组并正确收到为止。此时,所有数据对应用程序都是可用的。

TCP会记录它发送了多少字节,以及确认的字节,但它不会记录这些字节是如何分组的。实际上,有些实现在重传丢失分组的时候传送的数据可能比原来的多一些或少一些。这就足以支撑下面再次重复说明的内容了。

对TCP应用程序来说,就没有”分组”这种概念。如果应用程序的设计与TCP对数据的分组方式有所关联,就应该考虑重新设计这个应用程序了。

既然任意一次指定的读操作中返回的数据量都是不可预测的,就必须在应用程序中做好应对这种情况的准备。通常这不是什么问题。比如说,我们可能在用fgets这样标准的I/O库程序读取数据。在这种情况下,fgets会将字节流划分成行。图3-6显示了一个这样的例子。在其他情况下的确需要关注报文边界问题,而这些情况下边界都是由应用程序级维护的。

最简单的情况就是定长报文。在这种情况下,只需要读取报文中固定数量的字节就可以了。根据前面的讨论,读操作返回的字节数可能小于sizeof(msg)(图2-26D),所以只进行

recv(s, msg, sizeof(msg), 0); 

这样的简单调用是不够的。图2-27显示了处理这种情况的标准方法。

图2-27 函数readn

函数readn的用法与read非常相似,但在读到len字节,并从对等实体收到EOF,或出现错误之前,它是不会返回的。我们将其定义如下。

#include "etcp.h"  

int readn( SOCKET s, char *buf, size_t len );  

                                                            返回:读取的字节数,出错时返回-1  

readn使用的逻辑与从串行端口,或者从其他基于流的、在任意指定时间内可读取数据量都未知的源端,读取指定数量的字节所使用的逻辑一样,这不足为奇。实际上,在所有这些情况下都可以,也经常使用readn(用int代替SOCKET,用read代替recv)。

如果recv调用被信号中断,第11行和第12行的if语句

if ( error == EINTR)        /* interrupted? */   
    continue;               /* restart the read*/  

会重启recv调用。有些系统会自动重启被中断的系统调用,这种系统就不需要这两行程序了。从另一个角度来看,这两行代码也不会带来什么问题,因此为了实现最大限度的可移植性,最好还是把它们放在那里。

对必须支持可变长报文的应用程序来说,有两种可用的方法。第一种,可以用记录结束标记来分隔记录。如前所述,使用fgets这样标准的I/O程序将报文分成单个行时,就会发生这种情况。使用标准I/O程序时,很自然地会将新行作为记录结束标记使用。但使用这种方法通常会有一些问题。首先,除非在报文主体中从未用到记录结束标记,否则发送程序就要在报文中扫描这些标记,对其进行转义,或者编码,以免将其误认作记录结束标记。比如,如果将记录分隔字符RS作为记录结束标记使用,发送端就要搜索报文主体,找到所有RS字符,并对其进行转义,比如在前面加上一个\。这就意味着要转移数据以便为转义字符腾出位置。当然,还要对出现的所有转义字符进行转义。因此,如果用\作转义字符的话,就要将报文主体中出现的所有\都改成\。

在接收端,必须再次对整条报文进行扫描,这次要移除转义字符,并搜索(未转义的)记录结束标记。使用记录结束标记要对整条报文扫描两次,所以最好只在那些有”自然”记录结束标记的情况下使用,比如用换行符分隔文本行记录的时候。

另外一种处理可变记录的方法是在每条报文前面加上一个首部,这个首部(至少)包含下面的报文长度,如图2-28所示。

图2-28 可变长记录的格式

接收端应用程序分两部分读取报文。首先读取定长的报文首部,从首部解析出可变部分的长度,然后读取可变长部分。图2-29显示了一种简单的情况,其中首部只包含了记录的长度。

图2-29 读取可变长记录的函数readvrec

读取记录长度

6-8 将记录长度读入reclen中,如果readn返回的长度不等于interger类型的大小,readvrec就返回0(EOF),如果出错就返回 1。

9 将记录长度从网络字节序转换为主机字节序。更多相关内容请参见技巧28。

查看是否装得下记录

10-27 查看调用程序的缓冲区大小,验证它能否装下整条记录。如果缓冲区中没有足够的空间,就依次将长度为len的片段读入缓冲区,并将记录丢弃。丢弃记录之后,将errno设置为EMSGSIZE,readvrec返回 1。

读取记录

29-32 最后,读取记录本身。根据readn返回的是错误、不足计数还是成功返回,readvrec会向调用程序返回 1、0或者reclen。

readvrec是个很有用的函数,会在其他一些技巧中用到,所以将其定义记录如下。

#include "etcp. h"  

int readvrec( SOCKET s, char *buf, size_t len );  

                                                        返回:读取的字节数,或者在出错时返回-1  

图2-30显示了一个用readvrec从TCP连接中读取变长记录,并将其写入stdout的简单服务器代码。

图2-30 vrs--说明readvrec用法的服务器程序

10-17 初始化服务器,并接受一个连接。

20-24 调用readvrec读取下一个变长记录。如果出错,打印一条诊断信息,并读取下一条记录。如果readvrec返回一个EOF,就打印一条提示消息,服务器退出。

26 将记录写入stdout。

图2-31显示了对应的客户端程序,这个程序从其标准输入读取报文,附加上报文长度,然后将其发送给服务器。

图2-31 vrc--发送可变长报文的客户端程序

定义分组结构

6-10 定义packet结构,调用send时用它来装载报文及其长度。数据类型u_int32_t是一个无符号的32比特整数。由于Windows没有定义这种数据类型,所以在Windows版本的skel.h中有它的typedef。

对这个例子来说,还应该弄清楚另一个潜在的问题。假定编译器会严格按照我们的指令没有任何填充地将数据封装在一个结构中。因为第二个元素是一个字节数组,所以在大多数系统中这种假设都是有效的,但要小心,对编译器封装数据的方式进行的假设可能会引发一些问题。技巧24对其他一些同时发送两个或多个不同数据段的方法进行了讨论,那时会再次讨论这个问题。

连接、读取并逐行发送

12 客户端通过调用tcp_client连接到服务器。

13-21 调用fgets从stdin中读取一行数据,并将其放入报文分组的buf字段中。行的长度由对strlen的调用决定,将这个值转换成网络字节序后,放入报文分组的reclen字段中。最后,调用send向服务器发送分组。

发送这些由两个或多个部分组成的报文的另一种方法请参见技巧24。

在sparc上启动服务器vrs,然后在bsd上启动客户端vrc,来测试这些程序。将程序的运行并排显示出来,就可以看到客户端的输入,以及相应的服务器输出了。第4行还对错误消息进行了换行显示。

服务器缓冲区有10字节,所以发送11字节1, …, 0, 时,readvrec会返回一条错误。

##小结

初级网络程序员最常犯的错误之一就是无法理解TCP传送的是一个没有记录边界概念的字节流。这一点很重要,可以总结为TCP中没有用户可见的”分组”概念,它只是传送了一个字节流,我们无法准确地预测在一个特定的读操作中会返回多少字节。在这个技巧中,还对应用程序处理这种情况时所使用的几种策略进行了研究。

理解面向连接和无连接协议之间的区别

发表于 2015-03-24 | 分类于 TCP/IP

转载自《TCP/IP高效编程 改善网络程序的44个技巧》–技巧1


网络编程中最基本的概念就是面向连接(connection-oriented)和无连接(connectionless)协议。尽管本质上来说,两者之间的区别并不难理解,但对那些刚刚开始进行网络编程的人来说,却是个很容易混淆的问题。这个问题与上下文有些关联:很显然,如果两台计算机要进行通信,就必须以某种形式”连接”起来,那”无连接通信”又是什么意思呢?

答案是:面向连接和无连接指的都是协议。也就是说,这些术语指的并不是物理介质本身,而是用来说明如何在物理介质上传输数据的。面向连接和无连接协议可以,而且通常也确实会共享同一条物理介质。

如果两者的区别与承载数据的物理介质无关,又和什么有关呢?它们的本质区别在于,对无连接协议来说,每个分组的处理都独立于所有其他分组,而对面向连接的协议来说,协议实现则维护了与后继分组有关的状态信息。

无连接协议中的分组被称为数据报(datagram),每个分组都是独立寻址,并由应用程序发送的(但还请参考技巧30)。从协议的角度来看,每个数据报都是一个独立的实体,与在两个相同的对等实体之间传送的任何其他数据报都没有关系。

这并不是说从应用程序的角度来看,数据报也是独立的。简单的请求/应答协议,就是客户端向服务器发送一条请求,并收到一条应答,如果应用程序实现的功能比简单的请求/应答协议稍微复杂一点儿,就很可能需要维护数据报之间的状态。但问题的重点在于状态是由应用程序,而不是协议来维护的。图3-9显示了一个无连接服务器实例,在这个例子中,服务器维护了客户端发来的数据报之间的状态。

通常这就意味着客户端和服务器不会进行长期的对话–客户端发起一条请求,服务器回送一个应答。如果稍后客户端发起了另一条请求,协议会认为这是与第一个事务无关的独立事务。

这还意味着协议很可能是不可靠的。也就是说,网络会尽最大努力传送每一个数据报,但并不保证数据报不丢失、不延迟或者不错序传输。

另一方面,面向连接的协议则维护了分组之间的状态,使用这种协议的应用程序通常都会进行长期的对话。记住这些状态,协议就可以提供可靠的传输。比如,发送端可以记住哪些数据已经发送出去了但还未被确认,以及数据是什么时候发送的。如果在某段时间间隔内没有收到确认,发送端可以重传数据。接收端可以记住已经收到了哪些数据,并将重复的数据丢弃。如果分组不是按序到达的,接收端可以将其保存下来,直到逻辑上先于它的分组到达为止。

典型的面向连接协议有三个阶段。第一阶段,在对等实体间建立连接。接下来是数据传输阶段,在这个阶段中,数据在对等实体间传输。最后,当对等实体完成数据传输时,连接被拆除。

一种标准的类比是:使用面向连接的协议就像打电话,而使用无连接协议就像寄信。给朋友寄信时,每封信都是一个独立寻址且自包含的实体。邮局在处理这些信件时不会考虑到两个通信者之间的任何其他信件。邮局不会维护以往通信者的历史记录–也就是说,它不会维护信件之间的状态。邮局也不保证信件不丢失、不延迟、不错序。这种方式就对应于无连接协议发送数据报的方式。

[Haverlock, 2000]指出用明信片进行类比会更合适一些,因为写错地址的信件会被退回发信人,而(和典型的无连接协议数据报一样)明信片则不会。

现在来看看不是给朋友寄信,而是打电话时会发生些什么事情。首先,拨朋友的号码来发起呼叫。朋友应答,会说”嗨”之类的话,然后我们回应:”嗨,萨丽。我是亨利。”我们和朋友聊一会儿,然后互说再见并挂机。这是面向连接协议中发生的典型状况。在连接建立阶段,一端与其对等实体联系,交换初始问候信息,对会话中要用到的一些参数和选项进行沟通,然后连接进入数据传输阶段。

在电话交谈的过程中,两端用户都知道他们在和谁说话,因此没必要不停地说”这是亨利在跟萨丽说话”。也没必要在每次说话之前都拨一次朋友的电话号码–我们的电话已经连接起来了。同理,在面向连接协议的数据传输阶段,也没必要说明我们自己或对等实体的地址。连接为我们维护的状态中包含了这些地址。我们只要发送数据就行了,不需要考虑寻址或其他与协议相关的问题。

就像用电话交谈一样,连接的任一端完成数据的传输时,都要通知其对等实体。两端都完成传输时,要依次将连接拆除。

这种类比虽然很形象,但并不是非常贴切的。电话系统有实际的物理连接。而我们的”连接”则完全是想象的–它只是由两端记录的状态构成的。为了说明这一点,我们来看看当一个空闲连接一端的主机崩溃并重启时会发生什么情况。还有连接存在吗?从重启主机的角度来看,肯定是没有了。它对先前的连接一无所知。但它原来的对等实体仍然认为自己是连接着的,因为它仍然维护着与连接有关的状态,没有发生什么使那个状态失效的事件。

既然无连接协议有这么多的缺点,大家可能会奇怪,为什么还要使用这种协议呢?我们会看到,在很多情况下,使用无连接协议构建应用程序都是有意义的。比如,使用无连接协议可以很方便地支持一对多和多对一通信,而面向连接协议通常都需要多个独立的连接才能做到。但更重要的是,无连接协议是构建面向连接协议的基础。为了更具体地说明这个问题,也为了把讨论转回到本书的话题中来,我们来看看TCP/IP协议族。在技巧14中我们会看到,TCP/IP基于一个4层的协议栈,如图2-1所示。

图2-1 简化的TCP/IP协议栈

栈的底部是接口层,直接与硬件相连。栈的顶部是应用程序,比如Telnet、ftp和其他标准的以及用户编写的应用程序。如图所示,TCP和UDP都是构建在IP之上的。因此,IP是构建整个TCP/IP协议族的基础。但IP提供的是一种尽力而为的、不可靠的无连接服务。它接收来自其上层的分组,将它们封装在一个IP分组中,根据路由为分组选择正确的硬件接口,从这个接口将分组发送出去。一旦将分组发送出去了,IP就不再关心这个分组了。和所有无连接协议一样,它将分组发送出去之后就不再记得这个分组了。

这种简单性也是IP的主要优点。因为它对底层的物理介质没有作任何假设,所以在任何能够承载分组的物理链路上都可以运行IP。例如,IP可以运行在简单的串行链路、以太网和令牌环LAN、X.25和使用ATM(Asychronous Transfer Mode,异步转移模式)的WAN、CDPD(Cellular Digital Packet Data,无线蜂窝数字分组数据)网,以及很多其他网络上。尽管这些网络技术之间有很大的差异,但IP对它们一视同仁,除了认为它们可以转发分组之外没有对其作任何假设。这种机制隐含了很深的意义。IP可以运行在任何能够承载分组的网络上,所以整个TCP/IP协议族也可以。

现在我们来看看TCP是怎样利用这种简单的无连接服务来提供可靠的面向连接服务的。TCP的分组被称为段(segment),是放在IP数据报中发送的,因此,根本无法假定这些分组会抵达目的地,更不用说保证分组无损坏且以原来的顺序到达了。为了提供这种可靠性,TCP向基本的IP服务中添加了三项功能。首先,它为TCP段中的数据提供了校验和。这样有助于确保抵达目的地的数据在传输过程中不会被网络损坏。第二,它为每字节分配了一个序列号,这样,如果数据抵达目的地时真的错序了,接收端也能够按照恰当的顺序将其重装起来。

当然,TCP并没有为每字节都附加一个序列号。实际上,每个TCP段的首部都包含了段中第一字节的序列号。这样,就隐含地知道了段中其他字节的序列号。

第三,TCP提供了一种确认-重传机制,以确保最终每个段都会被传送出去。

确认/重试机制是到目前为止我们讨论的三种附加机制中最复杂的一种,我们来研究一下它是怎样工作的。

这里我们忽略了几个细节,基本没有涉及TCP协议的众多细微之处,以及如何用它们来提供健壮可靠的传输机制。RFC 793[Postel, 1981b]和RFC 1122[Braden, 1989]中有完整的细节描述。[Stevens, 1994]中的阐述则更容易为人接受。RFC 813[Clark, 1982]对 TCP窗口和确认机制进行了概略的讨论。

TCP连接的每一端都维护了一个接收窗口(receive window),接收窗口就是可以从对等实体接收的数据序列号范围。最小值表示窗口的左边界,是所期望的下一字节的序列号。最大值表示窗口的右边界,是TCP缓冲区空间所能容纳字节的最大编号。使用接收窗口而不只是所期望的下一字节计数器,就可以通过流量控制来提高可靠性。流量控制机制可以防止TCP传输的数据使其对等实体的缓冲区空间溢出。

TCP段到达时,序列号在接收窗口范围之外的所有数据都会被丢弃。其中包括先前已经收到的数据(序列号在接收窗口左边的数据),以及没有缓冲区空间存储的数据(序列号在接收窗口右边的数据)。如果段中第一个可接受字节不是所期望的下一字节,就说明这个段是错序的,大部分TCP应用程序都会将其放入队列,直到缺少的数据到达为止。如果段中第一个可接受字节是所期望的下一字节,就通知应用程序有数据可读,并在所期望的下一字节序列号上加上段中本次接受的字节数,对其进行更新。此时窗口向右滑动本次接受字节数的长度。最后,TCP向对等实体发送一条ACK,其中携带了它所期望的下一字节序列号。

比如,在图2-2A中,虚线框表示的接收窗口显示,所期望的下一字节序列号为4,而TCP希望接收9字节(4~12)。图2-2B显示的是收到字节4、5、6和7之后的接收窗口。窗口向右滑动了4个序列号,TCP的ACK会说明它接下来所期望的是序列号8。

图2-2 TCP接收窗口

同样是这种情况,现在从TCP发送端的角度来看。除了接收窗口之外,每个TCP还维护了一个发送窗口(send window)。发送窗口被划分成两部分:已发送但还未被确认的字节,以及可以发送但还未发送的字节。假设字节1~3已经被确认了,图2-3A显示的是与图2-2A中接收窗口相对应的发送窗口。字节4~7发送之后,确认之前,发送窗口如图2-3B所示。TCP还可以发送字节8~12而无须等待来自对等实体的ACK。发送了字节4~7之后,TCP会启动一个RTO(Retransmission Timeout,重传超时)定时器。如果在定时器超时之前这四个字节没有被确认,TCP就认为它们丢失了,并重新传送这四个字节。

很多实现并不记录一个特定的段中发送了哪些字节,因此重传段中包含的字节数可能会比原来的多。例如,如果字节8和9在RTO定时器超时之前发送出去了,这些应用程序就会重传字节4~9。

我们要注意这样一个事实:RTO定时器超时并不意味着原来的数据没有到达目的地。有可能是ACK丢失了,或者原来的段在网络中延迟的时间太长,以至于在其ACK到达之前RTO定时器就超时了。但这并不会造成什么问题,因为如果原来的数据确实到达了,那么重传的数据就会处于接收端TCP接收窗口范围之外,会被丢弃。

字节4~7确认后,发送端TCP会将其丢弃,并将发送窗口向右移动,如图2-3C所示。

图2-3 TCP发送窗口

对于编写应用程序的程序员来说,TCP提供了一种可靠的面向连接协议。更多关于可靠的具体含义的讨论请参见技巧9。

另一方面,UDP为编写应用程序的程序员提供了一种不可靠的无连接服务。事实上,UDP只向底层的IP协议中添加了两项功能。首先,它提供了一个可选的校验和来检测数据的损坏情况。尽管IP也有校验和,但它只对IP分组首部进行计算,所以,TCP和UDP也都提供了校验和来保护它们自己的首部和数据。UDP向IP添加的第二项特性就是端口的概念。

IP地址(这些地址通常都是以因特网标准的点分十进制表示法给出的,请参见技巧2)用来将一个IP数据报传送给一台特定的主机。数据报到达目的主机时,还需要将其数据传送给恰当的应用程序。例如,一个UDP分组的目标可能是回声服务,而另一个的目标则可能是时间查询服务。端口提供了一种将数据多路分解到正确目的应用程序的方式。每个TCP和UDP套接字都有一个与之相关的端口。应用程序可以通过显式的bind调用来设置这个端口,也可以由操作系统为其选择。分组到达时,内核会搜索其套接字列表,查找一个与分组中的协议、地址和端口号相匹配的套接字。如果找到了匹配的套接字,就由指定的协议(在我们所讨论的情形中,就是TCP或UDP)来处理数据,并将这些数据提供给所有打开了匹配套接字的应用程序。

如果有多个进程或线程打开了这个套接字,那么其中任何一个都可以读取数据,但读过一次之后,数据对其他进程或线程来说就不可用了。

回到与电话/寄信的类比中来,我们可以把TCP连接中的网络地址当作一个办公室总机的电话号码,把端口号当作办公室中某台正被呼叫的特定电话的分机号。同理,可以将UDP地址当作一座公寓楼的地址,并把端口号当作公寓楼大厅中的个人邮箱。

##小结

在这个技巧中,我们研究了无连接和面向连接协议的区别。我们看到,不可靠的无连接数据报协议是构建可靠的面向连接协议的基础,我们还简单介绍了可靠的TCP协议是如何构建在不可靠的IP协议上的。

我们还注意到,对TCP来说,连接完全是想象的。它是由端点所记忆的状态组成的,并不存在”物理”连接,而打电话的时候是有物理连接的。

IO和NIO

发表于 2015-03-22 | 分类于 unix网络编程

转载自这里

传统的socket IO中,需要为每个连接创建一个线程,当并发的连接数量非常巨大时,线程所占用的栈内存和CPU线程切换的开销将非常巨大。使用NIO,不再需要为每个线程创建单独的线程,可以用一个含有限数量线程的线程池,甚至一个线程来为任意数量的连接服务。由于线程数量小于连接数量,所以每个线程进行IO操作时就不能阻塞,如果阻塞的话,有些连接就得不到处理,NIO提供了这种非阻塞的能力。

小量的线程如何同时为大量连接服务呢?答案就是就绪选择。

这就好比到餐厅吃饭,每来一桌客人,都有一个服务员专门为你服务,从你到餐厅到结帐走人,这样方式的好处是服务质量好,一对一的服务,VIP啊,可是缺点也很明显,成本高,如果餐厅生意好,同时来100桌客人,就需要100个服务员,那老板发工资的时候得心痛死了,这就是传统的一个连接一个线程的方式。

老板是什么人啊,精着呢。这老板就得琢磨怎么能用10个服务员同时为100桌客人服务呢,老板就发现,服务员在为客人服务的过程中并不是一直都忙着,客人点完菜,上完菜,吃着的这段时间,服务员就闲下来了,可是这个服务员还是被这桌客人占用着,不能为别的客人服务,用华为领导的话说,就是工作不饱满。那怎么把这段闲着的时间利用起来呢。这餐厅老板就想了一个办法,让一个服务员(前台)专门负责收集客人的需求,登记下来,比如有客人进来了、客人点菜了,客人要结帐了,都先记录下来按顺序排好。每个服务员到这里领一个需求,比如点菜,就拿着菜单帮客人点菜去了。点好菜以后,服务员马上回来,领取下一个需求,继续为别人客人服务去了。这种方式服务质量就不如一对一的服务了,当客人数据很多的时候可能需要等待。但好处也很明显,由于在客人正吃饭着的时候服务员不用闲着了,服务员这个时间内可以为其他客人服务了,原来10个服务员最多同时为10桌客人服务,现在可能为50桌,60客人服务了。

这种服务方式跟传统的区别有两个:

  1. 增加了一个角色,要有一个专门负责收集客人需求的人。NIO里对应的就是Selector。
  2. 由阻塞服务方式改为非阻塞服务了,客人吃着的时候服务员不用一直侯在客人旁边了。传统的IO操作,比如read(),当没有数据可读的时候,线程一直阻塞被占用,直到数据到来。NIO中没有数据可读时,read()会立即返回0,线程不会阻塞。

NIO中,客户端创建一个连接后,先要将连接注册到Selector,相当于客人进入餐厅后,告诉前台你要用餐,前台会告诉你你的桌号是几号,然后你就可能到那张桌子坐下了,Selection Key就是桌号。当某一桌需要服务时,前台就记录哪一桌需要什么服务,比如1号桌要点菜,2号桌要结帐,服务员从前台取一条记录,根据记录提供服务,完了再来取下一条。这样服务的时间就被最有效的利用起来了。

TCP三次握手如果没有第三步会怎么样

发表于 2015-03-20 | 分类于 TCP/IP

没有第三步,那就是服务器发送给客户端的SYN没有收到回应,其实这就是SYN洪泛攻击。

SYN攻击属于DDOS攻击的一种,它利用TCP协议缺陷,通过发送大量的半连接请求,耗费CPU和内存资源。

SYN攻击除了能影响主机外,还可以危害路由器、防火墙等网络系统,事实上SYN攻击并不管目标是什么系统,只要这些系统打开TCP服务就可以实施。

服务器接收到连接请求(syn=j),将此信息加入未连接队列,并发送请求包给客户(syn=k,ack=j+1),此时进入SYN_RECV状态。当服务器未收到客户端的确认包时,重发请求包,一直到超时,才将此条目从未连接队列删除。配合IP欺骗,SYN攻击能达到很好的效果,通常,客户端在短时间内伪造大量不存在的IP地址,向服务器不断地发送syn包,服务器回复确认包,并等待客户的确认,由于源地址是不存在的,服务器需要不断的重发直至超时,这些伪造的SYN包将长时间占用未连接队列,正常的SYN请求被丢弃,目标系统运行缓慢,严重者引起网络堵塞甚至系统瘫痪。

为什么malloc时需要指定size,对应的free时不需要指定size

发表于 2015-03-20 | 分类于 c

当调用malloc(size)时,实际分配的内存大小大于size字节,这是因为在分配的内存区域头部有类似于

struct control_block {
    unsigned size;
    int used;
};

这样的一个结构,如果malloc函数内部得到的内存区域的首地址为void *p,那么它返回给你的就是p + sizeof(control_block),而调用free(q)的时候,该函数把p减去sizeof(control_block),然后就可以根据
((control_blcok*)q)->size得到要释放的内存区域的大小。这也就是为什么free只能用来释放malloc分配的内存,如果用于释放其他的内存,会发生未知的错误。

进程的状态及转换

发表于 2015-03-20 | 分类于 linux

##进程的基本状态

  1. 执行状态(Running):进程占用处理机,进程的程序正在执行。单处理机系统中只能有一个进程处于执行状态,多处理机系统中可能有多个进程处于执行状态。
  2. 阻塞状态(Blocked):也叫等待或睡眠状态,是进程由于等待某种事件的发生而处于暂停执行的状态。如进程因等待I/O的完成、等待缓冲空间等。
  3. 就绪状态(Ready):进程已分配到处理机以外的所有必要资源,具备了执行的所有条件。可能会有多个进程处于就绪状态,排成就绪队列。

##新状态和终止状态

  1. 新状态:进程刚刚建立,还没有送入就绪队列的状态。

  2. 终止状态:一个进程已正常结束或非正常结束,OS已将它从就绪队列中移出,还未将它撤销时的状态。

##进程状态的转换

进程在执行期间可以多次处于就绪状态和执行状态,也可多次处于阻塞状态,但处于新状态只有一次。

  1. 新状态->就绪状态:当就绪队列允许接纳新进程时,系统便把处于新状态的进程移入就绪队列。

  2. 就绪态->执行状态:进程调度程序为处于就绪状态的进程分配处理机后,该进程进入执行状态。

  3. 执行态->阻塞状态:正在执行的进程因需要等待某事件而无法执行。

  4. 阻塞状态->就绪态:进程所等待的事件发生了,进程就从阻塞状态进入就绪状态。

  5. 执行态->就绪状态:正在执行的进程因时间片用完而被暂停执行;或者在可抢占调度方式中,一个优先权高的进程到来后,正在执行的低优先权的进程被强制撤下处理机,转换为就绪状态。

  6. 执行态->终止状态:一个进程已完成或发生某种特殊事件,进程将变为终止状态。

进程调度算法

发表于 2015-03-20 | 分类于 linux

调度也称dispatcher, 内核的主要职责之一就是决定该轮到哪个任务运行了。多数实时内核是基于优先级调度算法的。每个任务根据其重要程度的不同被赋予一定的优先级。基于优先级的调度法指CPU 总是让处在就绪态的优先级最高的任务先运行,然而究竟何时让高优先级任务掌握CPU 的使用权有两种不同的情况。这要看用的是什么类型的内核,是非占先式,还是占先式的内核。一个良好的任务调度算法应该主要体现在以下几个方面

  1. 公平保证每个进程得到合理的CPU 时间
  2. 高效使CPU保持忙碌状态即总是有进程在CPU上运行
  3. 响应时间使交互用户的响应时间尽可能短
  4. 周转时间使批处理用户等待输出的时间尽可能短
  5. 吞吐量使单位时间内处理的进程尽可能多

很显然在任何操作系统中这几个目标不可能同时达到,所以不同的操作系统会在这几个方面中做出相应的取舍,从而确定自己的调度算法,常用的处理机调度算法有:

  1. 先来先服务FCFS
  2. 短作业优先SJF
  3. 优先级
  4. 时间片轮转法
  5. 多级队列法
  6. 多级反馈队列法

先来先服务:FCFS 是最简单的CPU 调度算法,即按进程到来的先后次序进行调度,这样在系统中等待时间最长的进程被优先调度,而不管其所需运行时间的长短。

作业优先:SJF 算法是指当CPU 可供使用时,SJF 算法把CPU 分给需要运行时间最短的进程。

多级队列调度算法:把就绪队列划分成几个单独的队列,一般根据进程的某些特性如内存大小和进程类型,永久性地把各个进程分别链入其中某一个队列中,每个队列都有自己的调度算法,此外在各个队列之间也要进行调度。通常采用固定优先级的抢占式调度,例如某系统中有5 个队列,各个队列的优先级自上而下降低,只有当前3 个队列中都为空的时候,队列4 中的进程才可以运行,而当队列4 中的进程在运行时,如果队列1 中进入了一个就绪进程,则队列4 中的进程要立刻让出CPU 使用权。多级反馈队列法允许进程在各队列间移动,其基本思想是把具有不同CPU工作时间这一特性的进程区分开来,如果一个进程要使用很长的CPU 时间,则应把它移至较低级的队列中,这种方式把I/O 繁忙型和交互式进程放在较高优先级的队列中,同样在低优先级队列中长时间等待的进程可以移到较高优先级队列中,UNIX 系统采用的是多级反馈队列轮转法。

时间片轮转调度法:round-robin scheduling
当两个或两个以上任务有同样优先级,内核允许一个任务运行事先确定的一段时间叫做时间额度quantum ,然后切换给另一个任务也叫做时间片调度time slicing ,内核在满足以下条件时把CPU 控制权交给下一个任务就绪态的任务, 当前任务已无事可做,当前任务在时间片还没结束时已经完成了。轮转法主要是为分时系统设计的,其中时间片是一个重要的参数不能取的过大或过小,通常为10 至100ms 数量级,就绪队列可以看成是一个环形队列,CPU 调度程序轮流地把CPU 分给就绪队列中地每个进程,时间长度为一个时间片Linux 操作系统就是采用时间片轮转的调度算法。

socket的长连接和短连接

发表于 2015-03-20 | 分类于 unix网络编程

所谓长连接,指在一个TCP连接上可以连续发送多个数据包,在TCP连接保持期间,如果没有数据包发送,需要双方发检测包以维持此连接,一般需要自己做在线维持。

短连接是指通信双方有数据交互时,就建立一个TCP连接,数据发送完成后,则断开此TCP连接,一般银行都使用短连接。

比如http的,只是连接、请求、关闭,过程时间较短,服务器若是一段时间内没有收到请求即可关闭连接。

其实长连接是相对于通常的短连接而说的,也就是长时间保持客户端与服务端的连接状态。

##长连接与短连接的操作过程

通常的短连接操作步骤是:
连接→数据传输→关闭连接;

而长连接通常就是:
连接→数据传输→保持连接(心跳)→数据传输→保持连接(心跳)→……→关闭连接;
这就要求长连接在没有数据通信时,定时发送数据包(心跳),以维持连接状态,短连接在没有数据传输时直接关闭就行了

##什么时候用长连接,短连接?

长连接多用于操作频繁,点对点的通讯,而且连接数不能太多情况,。每个TCP连接都需要三步握手,这需要时间,如果每个操作都是先连接,再操作的话那么处理速度会降低很多,所以每个操作完后都不断开,次处理时直接发送数据包就OK了,不用建立TCP连接。例如:数据库的连接用长连接, 如果用短连接频繁的通信会造成socket错误,而且频繁的socket 创建也是对资源的浪费。

而像WEB网站的http服务一般都用短链接,因为长连接对于服务端来说会耗费一定的资源,而像WEB网站这么频繁的成千上万甚至上亿客户端的连接用短连接会更省一些资源,如果用长连接,而且同时有成千上万的用户,如果每个用户都占用一个连接的话,那可想而知吧。所以并发量大,但每个用户无需频繁操作情况下需用短连好。

##发送接收方式

###异步
报文发送和接收是分开的,相互独立的,互不影响。这种方式又分两种情况:

  1. 异步双工:接收和发送在同一个程序中,由两个不同的子进程分别负责发送和接收
  2. 异步单工:接收和发送是用两个不同的程序来完成。
    ###同步
    报文发送和接收是同步进行,既报文发送后等待接收返回报文。 同步方式一般需要考虑超时问题,即报文发出去后不能无限等待,需要设定超时时间,超过该时间发送方不再等待读返回报文,直接通知超时返回。

在长连接中一般是没有条件能够判断读写什么时候结束,所以必须要加长度报文头。读函数先是读取报文头的长度,再根据这个长度去读相应长度的报文。具体原因可以参考这里

##补充:HTTP的长连接

HTTP也可以建立长连接的,使用Connection:keep-alive,HTTP 1.1默认进行持久连接。HTTP1.1和HTTP1.0相比较而言,最大的区别就是增加了持久连接支持(貌似最新的 http1.0 可以显示的指定 keep-alive),但还是无状态的,或者说是不可以信任的。

1…789…18
You Wangqiu

You Wangqiu

世之奇伟、瑰怪,非常之观,常在于险远,而人之所罕至焉,故非有志者不能至也

171 日志
21 分类
24 标签
GitHub 知乎 E-Mail
© 2018 You Wangqiu
由 Hexo 强力驱动
主题 - NexT.Muse