Linux IO复用—select poll 和 epoll

在Socket编程时,为了处理大量客户的连接请求,需要使用非阻塞I/O和端口复用,select、poll和epoll是Linux API提供的I/O复用方式。其实在*nix下的网络并发方法向来不缺,比如典型的Apache模型(Process Per Connection,简称PPC),TPC(Thread Per Connection)模型,这两种模型思想类似,就是利用了多进程、多线程概念,让进来的每一个连接去干别的事情去。但是连接多了以后,首先需要较大的内存,且进程/线程切换开销会非常大,因此这类模型能接受的最大连接数都不会太高。

Linux 2.6中加入了epoll之后(据说Windows下使用的是IOCP,但是我没使用过),在高性能服务器领域中得到广泛的应用,主要原因就是高效。在讲epoll之前,我们先总结下select、poll,因为epoll其实也就是他们的增强版本,比如select是一个系统调用,而epoll是个模块,由三个系统调用组成,内核中由文件系统实现。

select

select的第一个参数为fdset集合中最大描述符值加1,select对应于内核中的sys_select调用,sys_select首先将第二三四个参数指向的fd_set拷贝到内核,然后对每个被SET的描述符进行poll,并记录在临时结果中(fdset),如果有事件发生,select会将临时结果写到用户空间并返回,当轮询一遍后没有任何事件发生,如果指定了超时时间,则select会睡眠到超时,睡眠结束后再进行下一次轮询,并将临时结果写到用户空间,然后返回。select返回后,需要逐一检查关注的描述符是否被SET(事件是否发生)。

int select(int nfds, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);
1
2
3
4
5
6
参数                  描述
nfds             sets的文件描述符的最大值
readfds          fd_set type 类型,只读的描述符集
writefds         fd_set type 类型,只写的描述符集
errorfds         fd_set type 类型,错误的描述符集
timeout          超时等待时间

为了维护fd_set类型的参数,会使用下面四个宏:FD_SET(), FD_CLR(), FD_ZERO() 和 FD_ISSET()。

1
2
3
4
5
6
7
8
9
10
11
12
typedef struct fd_set
{
u_int fd_count;
int fd_array[FD_SETSIZE];
}
//fd_array可SIZE*8个socket
int FD_ISSET(int fd,fd_set *fdset);
//返回值:若fd在描述符集中则返回非0值,否则返回0

void FD_CLR(int fd,fd_set *fdset); //fd指描述符
void FD_SET(int fd,fd_set *fdset);
void FD_ZERO(fd_set *fdset);

select 函数监视的文件描述符分3类,分别是writefds,readfds、和exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据可读、可写、或者except)、或者超时(timeout指定等待的时间,timeout== NULL表示永远等待,timeout == 0表示不等待、立即返回,其他表示等待的时间)。当 select 函数返回后,可以通过遍历 fdset ,来找到就绪的描述符。

select 的一个优点就是跨平台,缺点就是单个进程能够监视的文件描述符的数量存在最大限制,linux下一般为1024,Windows下好像无此限制,虽然可以修改这一限制,但是这样也会造成效率低下。

运行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
int sock;
FILE *fp;
struct fd_set fds;
struct timeval timeout={3,0}; //select等待3秒,3秒轮询,要非阻塞就置0
char buffer[256]={0}; //256字节的接收缓冲区
//假定已经建立UDP连接,具体过程不写,简单,当然TCP也同理,主机ip和port都已经给定,要写的文件已经打开
sock=socket(...);
bind(...);
fp=fopen(...);
while(1)
{
FD_ZERO(&fds); //每次循环都要清空集合,否则不能检测描述符变化
FD_SET(sock,&fds); //添加描述符
FD_SET(fp,&fds); //同上
maxfdp=sock>fp?sock+1:fp+1; //描述符最大值加1

// for(int i =0 ;i < maxfds; i++) if(FD_ISSET()) { }
switch(select(maxfdp,&fds,&fds,NULL,&timeout)) //select使用
{
case -1: exit(-1);break; //select错误,退出程序
case 0:break; //再次轮询
default:
if(FD_ISSET(sock,&fds)) //测试sock是否可读,即是否网络上有数据
{
recvfrom(sock,buffer,256,.....);//接受网络数据
if(FD_ISSET(fp,&fds)) //测试文件是否可写
fwrite(fp,buffer...);//写入文件
......
}
}
}

注意:每次select 有数据要遍历全部socket,每次select之前要重置fds的值。

poll

poll函数类似于 select,可用于任何类型的文件描述符,与 select 不同,poll不是为每个状态(可读性、可写性和异常状态)构造一个描述符集,而是构造一个pollfd 结构数组向内核传递需要关注的事件,故没有描述符个数的限制,每个数组元素指定一个描述符编号以及对其所关心的状态,pollfd中的events字段和revents字段分别用于标示关注的事件和发生的事件。

poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,poll返回后,需要对pollfd中的每个元素检查其revents值,来判断事件是否发生。

返回值:

-1:有错误产生
0:超时时间到,而且没有描述符有状态变化
>0:有状态变化的描述符个数

1
int poll(struct pollfd fdarray[],nfds_t nfds,int timeout);
1
2
3
4
5
struct pollfd{
int fd; //需要检查的文件描述符
short events; //等待的需要测试事件
short revents; //实际发生了的事件,也就是返回结果
};

应将每个数组元素的events成员设置为下图所示的值。通过这些值告诉内核我们对该描述符关心的是什么。返回时,内核设置revents成员,以说明对于该描述符已经发生了什么事件。(注意,poll没有更改events成员,这与select不同,select修改其参数以指示哪一个描述符已准备好了。)

poll的events和revents标志:


timeout == -1 永远等待。当所指定的描述符中的一个已准备好,或捕捉到一个信号时则返回。如果捕捉到一个信号,则poll返回-1,errno设置为EINTR timeout == 0 不等待 timeout > 0 等待timeout毫秒,如果已超时但是还没有一个描述符准备好,则返回值是0。
运行过程(与select相似):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
struct pollfd fds[IN_FILES];
char buf[MAX_BUFFER_SIZE];
int i,res,real_read, maxfd;
fds[0].fd = 0;
if((fds[1].fd=open("data1",O_RDONLY|O_NONBLOCK)) < 0)
{
fprintf(stderr,"open data1 error:%s",strerror(errno));
return 1;
}
if((fds[2].fd=open("data2",O_RDONLY|O_NONBLOCK)) < 0)
{
fprintf(stderr,"open data2 error:%s",strerror(errno));
return 1;
}
for (i = 0; i < IN_FILES; i++)
{
fds[i].events = POLLIN;
}
while(fds[0].events || fds[1].events || fds[2].events)
{
if (poll(fds, IN_FILES, TIME_DELAY) <= 0)
{
printf("Poll error\n");
return 1;
}
for (i = 0; i< IN_FILES; i++)
{
if (fds[i].revents)
{
memset(buf, 0, MAX_BUFFER_SIZE);
real_read = read(fds[i].fd, buf, MAX_BUFFER_SIZE);
if (real_read < 0)
{
if (errno != EAGAIN)
{
return 1;
}
}
else if (!real_read)
{
close(fds[i].fd);
fds[i].events = 0;

......

};

epoll

epoll通过epoll_create创建一个用于epoll轮询的描述符,通过epoll_ctl添加/修改/删除事件,通过epoll_wait 检查事件,epoll_wait 的第二个参数用于存放结果。

epoll与select、poll不同,首先,其不用每次调用都向内核拷贝事件描述信息,在第一次调用后,事件信息就会与对应的epoll描述符关联起来。另外epoll不是通过轮询,而是通过在等待的描述符上注册回调函数,当事件发生时,回调函数负责把发生的事件存储在就绪事件链表中,最后写到用户空间。

epoll返回后,该参数指向的缓冲区中即为发生的事件,对缓冲区中每个元素进行处理即可,而不需要像poll、select那样进行轮询检查。

系统调用:

1
2
//创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。
int epoll_create(int size);
1
2
3
4
5
6
7
8
/*
epoll的事件注册函数,它与select()是在监听事件时告诉内核要监听什么类型的事件不同,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动作,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是需要监听的fd,第四个参数是告诉内核需要监听什么事。
*/
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

struct epoll_event结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
typedef union epoll_data {
void *ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;

struct epoll_event
{
__uint32_t events; //Epoll events
epoll_data_t data; //User data variable
};

`
events可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里

1
2
3
4
/*
等待事件的产生,类似于select()调用,参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法是永久阻塞),该函数返回需要处理的事件数目
*/
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

运行过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
for (n = 0; n < nfds; ++n) {
if (events[n].data.fd == listener) { // 如果是主socket的事件的话,则表示
// 有新连接进入了,进行新连接的处理。
client = accept(listener, (struct sockaddr *) &local, &addrlen);
if (client < 0){
perror("accept");
continue;
}
setnonblocking(client); // 将新连接置于非阻塞模式
ev.events = EPOLLIN | EPOLLET; // 并且将新连接也加入EPOLL的监听队列。
// 注意,这里的参数EPOLLIN | EPOLLET并没有设置对写socket的监听,
// 如果有写操作的话,这个时候epoll是不会返回事件的,如果要对写操作
// 也监听的话,应该是EPOLLIN | EPOLLOUT | EPOLLET
ev.data.fd = client;
if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
// 设置好event之后,将这个新的event通过epoll_ctl加入到epoll的监听队列里面,
// 这里用EPOLL_CTL_ADD来加一个新的epoll事件,通过EPOLL_CTL_DEL来减少一个
// epoll事件,通过EPOLL_CTL_MOD来改变一个事件的监听方式。
fprintf(stderr, "epoll set insertion error: fd=%d0, client);
return -1;
}
} else if (event[n].events & EPOLLIN) { // 如果是已经连接的用户,并且收到数据,
// 那么进行读入
int sockfd_r;
if ((sockfd_r = event[n].data.fd) < 0)
continue;
read(sockfd_r, buffer, MAXSIZE);
// 修改sockfd_r上要处理的事件为EPOLLOUT
ev.data.fd = sockfd_r;
ev.events = EPOLLOUT | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
} else if (event[n].events & EPOLLOUT) { // 如果有数据发送
int sockfd_w = events[n].data.fd;
write(sockfd_w, buffer, sizeof(buffer));
// 修改sockfd_w上要处理的事件为EPOLLIN
ev.data.fd = sockfd_w;
ev.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd, EPOLL_CTL_MOD, sockfd_r, &ev)
}
do_use_fd(events[n].data.fd);
}

可简单归结为:

1
2
3
4
5
6
7
8
int fd = epoll_create(xxA); //xxA可监听的socket
struct epoll_event events[xxxB];//可返回的事件数
while(1){
int nfds = epoll_wait( ); //wait event occur
for(int i=0; i<nfds; i++){
…. }//end for

}//end while

epoll_wait返回的都是有效数据,可直接从struct epoll_events[]中获取事件,效率高。每次取事件后,要重新注册此socket的事件epoll(epoll_ctl)。

参考资料:

《UNIX环境高级编程》

http://zh.wikipedia.org/wiki/Select_(Unix)

http://zh.wikipedia.org/wiki/Epoll

http://www.cnblogs.com/xuxm2007/archive/2011/08/15/2139809.html

http://www.cnblogs.com/bigwangdi/p/3182958.html

记录下传输层协议TCP和UDP的一些特性

在TCP/IP中能够实现传输层功能的、具有代表性的协议是TCP和UDP。

TCP

TCP是面向连接的、可靠的流协议。流就是指不间断的数据结构,你可以把它想象成排水道中的水流。当应用程序采用TCP发送消息时,虽然可以保证发送的顺序,但还是犹如没有任何间隔的数据流发送给接收端。TCP充分地实现了数据传输时各种控制功能,可以进行丢包时的重发控制,还可以对次序乱掉的分包进行顺序控制。而这些在UDP中都没有。此外,TCP作为一种面向有连接的协议,只有在确认通信对端存在时才会发送数据,从而可以控制通信流量的浪费。根据TCP的这些机制,在IP这种无连接的网络上也能够实现高可靠性的通信。

TCP为提供可靠性传输,实行“顺序控制”或“重发控制”机制。此外还具备“流控制(流量控制)”、“拥塞控制”、提供网络利用率等众多功能。

TCP通过检验和、序列号、确认应答、重发控制、连接管理以及窗口控制等机制实现可靠性传输。

通过序列号与确认应答提高可靠性

在TCP中,当发送端的数据到达接收主机时,接收端主机会返回一个已收到消息的通知。这个消息叫做确认应答(ACK)。

TCP通过肯定的确认应答(ACK)实现可靠的数据传输。当发送端将数据发出之后会等待对端的确认应答。如果有确认应答,说明数据已经成功到达对端。反之,则数据丢失的可能性很大。在一定时间内没有等到确认应答,发送端就可以认为数据已经丢失,并进行重发。由此,即使产生了丢包,仍然能够保证数据能够到达对端,实现可靠传输。未收到确认应答并不意味着数据一定丢失。也有可能是数据对方已经收到,只是返回的确认应答在途中丢失。这种情况也会导致发送端因没有收到确认应答,而认为数据没有到达目的地,从而进行重新发发送。

上述这些确认应答处理、重发控制以及重复控制等功能都可以通过序列号实现。序列号是按顺序给发送数据的每一个字节(8位字节)都标上号码的编号。接收端查询接收数据TCP首部中的序列号和数据的长度,将自己下一步应该接收的序号作为确认应答返送回去。就这样,通过序列号和确认应答号,TCP可以实现可靠传输。

重发超时如何确实

重发超时是指在重发数据之前,等待确认应答到来的那个特定时间间隔。如果超过了这个时间仍未收到确认应答,发送端将进行数据重发。那么这个重发超时的具体时间长度又是如何确定的呢?

最理想的是,找到一个最小时间,它能保证“确认应答一定能在这个时间内返回”。然而这个时间长短随着数据包途径的网络环境的不同而有所变化。例如在高速的LAN中时间相对较短,而在长距离的通信当中应该比LAN要长一些。即使是在同一个网络中,根据不同时段的网络堵塞程度时间的长短也会发生变化。TCP要求不论处在何种网络环境下都要提供高性能通信,并且无论网络拥堵情况发生何种变化,都必须保持这一特性。为此,它在每次发包时都会计算往返时间及其偏差。将这个往返时间和偏差相加重发超时的时间,就是比这个总和要稍大一点的值。

在BSD的Unix以及Windows系统中,超时都以0.5秒为单位进行控制,因此重发超时都是0.5秒的整数倍。不过,由于最初的数据包还不知道往返时间,所以其重发超时一般设置为6秒左右。数据被重发之后若还是收不到确认应答,则进行再次发送。此时,等待确认应答的时间将会以2倍、4倍的指数函数延长。此外,数据也不会被无限、反复地重发。达到一定重发次数之后,如果仍没有任何确认应答返回,就会判断为网络或对端主机发生了异常,强制关闭连接。并且通知应用通信异常强行终止。

连接管理

UDP是一种面向无连接的通信协议,因此不检查对端是否可以通信,直接将UDP包发送出去。TCP与此相反,它会在数据通信之前,通过TCP首部发送一个SYN包作为建立连接的请求等待确认应答。如果对端发来确认应答,则认为可以进行数据通信。如果对端的确认应答未能到达,就不会进行数据通信。此外,在通信结束时会进行断开连接的处理(FIN包)。

可以使用TCP首部用于控制的字段来管理TCP连接。一个连接的建立与断开,正常过程至少需要来回发送7个包才能完成。

TCP以段为单位发送数据

在建立TCP连接的同时,也可以确认发送数据包的单位,我们也可以称其为“最大消息长度”(MSS:Maximum Segment Size)。最理想的情况是,最大消息长度正好是IP中不会被分片处理的最大数据长度。

TCP在传送大量数据时,是以MSS的大小将数据进行分割发送。进行重发时也是以MSS为单位。

MSS是在三次握手的时候,在两端主机之间被计算得出。两端的主机在发出建立连接的请求时,会在TCP首部中写入MSS选项,告诉对方自己的接口能够适应MSS的大小。然后会在两者之间选择一个较小的值投入使用。

利用窗口控制提高速度

TCP以1个段为单位,每发一个段进行一次确认应答的处理,这样的传输方式有一个缺点,那就是,包的往返时间越长通信性能就越低。

为解决这个问题,TCP引入了窗口这个概念。即使在往返时间较长的情况下,它也能控制网络性能的下降。确认应答不再是以每个分段,而是以更大的单位进行确认时,转发时间将会大幅度的缩短。也就是说,发送端主机,在发送了一个段以后不必要一直等待确认应答,而是继续发送。

窗口大小就是无需等待确认应答而可以继续发送数据的最大值,如上,窗口大小为4个段。这个机制实现了使用大量的缓冲区,通过对多个段同时进行确认应答的功能。
收到确认应答的情况下,将窗口滑动到确认应答中的序列号的位置。这样可以顺序地将多个段同时发送提高通信性能。这种机制也被成为滑动窗口控制。

流控制

发送端根据自己的实际情况发送数据。但是,接收端可能收到的是一个毫无关系的数据包又可能会在处理其他问题上花费一些时间。因此在为这个数据包做其他处理时会耗费一些时间,甚至在高负荷的情况下无法接收任何数据。如此一来,如果接收端将本应该接收的数据丢弃的话,就又会触发重发机制,从而导致网络流量的无端浪费。

为了防止这种现象的发生,TCP提供一种机制可以让发送端根据接收端的实际接收能力控制发送的数据量。这就是所谓的流控制。它的具体操作是,接收端主机向发送端主机通知自己可以接收数据的大小,于是发送端会发送不超过这个限度的数据。该大小限度就被称作窗口大小。

TCP首部中,专门有一个字段用来通知窗口大小。接收主机将自己可以接收的缓冲区大小放入这个字段中通知给发送端。这个字段的值越大,说明网络的吞吐量越高。

不过,接收端的这个缓冲区一旦面临数据溢出时,窗口大小的值也会随之被设置为一个更小的值通知给发送端,从而控制数据发送量。也就是说,发送端主机会根据接收端主机的指示,对发送数据的量进行控制。这也就形成了一个完整的TCP流控制(流量控制)。

拥塞控制

有了TCP的窗口控制,收发主机之间即使不再以一个数据段为单位发送确认应答,也能够连续发送大量数据包。然而,如果在通信刚开始时就发送大量数据,也可能会引发其他问题。在网络出现堵塞时,如果突然发送一个较大量的数据,极有可能导致整个网络的瘫痪。TCP为了防止该问题的出现,在通信一开始时就会通过一个叫做慢启动的算法得出的数值,对发送数据量进行控制。

首先,为了在发送端调节所要发送数据的量,定义了一个叫做“拥塞窗口”的概念。于是在慢启动的时候,将这个拥塞窗口的大小设置为1个数据段(1MSS)发送数据,之后每收到一次确认应答(ACK),拥塞窗口的值就加1.在发送数据包时,将拥塞窗口的大小与接收端主机通知的窗口大小做比较,然后按照它们当中较小的那个值,发送比其还要小的数据量。

提高网络利用率的规范

Nagle算法:

TCP中为了提高网络的利用率,经常使用一个叫做Nagle的算法。该算法是指发送端即使还有应该发送的数据,但如果这部分数据很少的话,则进行延迟发送的一种处理机制。具体来说,就是仅在下列任意一种条件下才能发送数据。如果两个条件都不满足,那么暂时等待一段时间以后再进行数据发送。

1>已发送的数据都已经收到确认应答时

2>可以发送最大段长度(MSS)的数据时

延迟确认应答:

当某个接收端收到这个小窗口的通知以后,会以它为上限发送数据,从而又降低了网络的利用率。为此,引入了一个方法,那就是收到数据以后并不立即返回确认应答,而是延迟一段时间的机制。

捎带应答:

捎带应答是指在同一个TCP包中既发送数据又发送确认应答的一种机制。由此,网络的利用率会提高,计算机的负荷也会减轻。不过,确认应答必须得等到应用处理完数据并将作为回执的数据返回为止,才能进行捎带应答。

UDP:

UDP是User Datagram Protocol的缩写。UDP是不具有可靠性的数据报协议。细微的处理它会交给上层的应用去完成。在UDP的情况下,虽然可以确保发送消息的大小,却不能保证消息一定会到达。因此,应用有时会根据自己的需要进行重发处理。

UDP不提供复杂的控制机制,利用IP提供面向无连接的通信服务。并且它是将应用程序发来的数据在收到的那一刻,立即按照原样发送到网络上的一种机制。

即使是出现网络堵塞的情况下,UDP也无法进行流量控制等避免网络拥塞的行为。此外,传输途中即使出现丢包,UDP也不负责重发。甚至当出现包的到达顺序乱掉时也没有纠正的功能。如果需要这些细节控制,那么不得不交由采用UDP的应用程序去处理。UDP有点类似于用户说什么听什么的机制,但是需要用户充分考虑好上层协议类型并制作相应的应用程序。因此,也可以说,UDP按照“制造程序的那些用户的指示行事”。

由于UDP面向无连接,它可以随时发送数据。再加上UDP本身的处理既简单又高效,因此经常用于以下几个方面:

1>包总量较少的通信(DNS、SNMP等)

2>视频、音频等多媒体通信(即时通信)

3>限定于LAN等特定网络中的应用通信

4>广播通信(广播、多播)

可能有人会认为,鉴于TCP是可靠的传输协议,那么它一定优于UDP。其实不然。TCP与UDP的优缺点无法简单地、绝对地去做比较。那么,对这两种协议应该如何加以区分使用呢?下面,我就对此问题做一简单说明。

TCP用于在传输层有必要实现可靠的情况。由于它是面向有连接并具备顺序控制、重发控制等机制的,所以它可以为应用提供可靠传输。

而UDP主要用于那些对高速传输和实时性有较高要求的通信或广播通信。我们拿通过IP电话进行通话作为例子。如果使用TCP,数据在传送途中如果丢失会重发,但这样无法刘畅地传输通话人的声音,会导致无法进行正常交流。而采用UDP,它不会进行重发处理。从而也就不会有声音大幅度延迟到达的问题。即使有部分数据丢失,也只是会影响某一小部分的通话。此外,在多播与广播通信中也使用UDP而不是TCP。RIP、DHCP等基于广播的协议也要依赖于UDP。因此,TCP和UDP应该根据应用的目的按需使用。

实现一个LRU Cache

1.什么是Cache和LRU Cache

狭义的Cache指的是位于CPU和主存间的快速RAM, 通常它不像系统主存那样使用DRAM技术,而使用昂贵但较快速的SRAM技术。 广义上的Cache指的是位于速度相差较大的两种硬件之间, 用于协调两者数据传输速度差异的结构。除了CPU与主存之间有Cache, 内存与硬盘之间也有Cache,乃至在硬盘与网络之间也有某种意义上的Cache── 称为Internet临时文件夹或网络内容缓存等。

CPU中Cache能极大提高存取数据和指令的时间,让整个存储器(Cache和内存)既有Cache的高速度,又能有内存的大容量;操作系统中的内存Page中使用的Cache能使得频繁读取的内存磁盘文件较少的被置换出内存,从而提高访问速度;数据库中数据查询也用到Cache来提高效率;即便是Powerbuilder的DataWindow数据处理也用到了Cache的类似设计。

Cache的算法设计常见的有FIFO(first in first out)和LRU(least recently used)。LRU是Least Recently Used的缩写,意思是最近最少使用,它是一种Cache替换算法。Cache的容量有限,因此当Cache的容量用完后,而又有新的内容需要添加进来时, 就需要挑选并舍弃原有的部分内容,从而腾出空间来放新内容。LRU Cache 的替换原则就是将最近最少使用的内容替换掉。其实,LRU译成最久未使用会更形象, 因为该算法每次替换掉的就是一段时间内最久没有使用过的内容。

注:

动态随机存取存储器(Dynamic Random Access Memory,DRAM)是一种半导体存储器,主要的作用原理是利用电容内存储电荷的多寡来代表一个二进制比特(bit)是1还是0。由于在现实中电容会有漏电的现象,导致电位差不足而使记忆消失,因此除非电容经常周期性地充电,否则无法确保记忆长存。由于这种需要定时刷新的特性,因此被称为“动态”存储器。相对来说,“静态”存储器(SRAM)只要存入数据后,纵使不刷新也不会丢失记忆。

静态随机存取存储器(Static Random-Access Memory, SRAM)是随机存取存储器的一种。所谓的“静态”,是指这种存储器只要保持通电,里面储存的数据就可以恒常保持[1]。相对之下,动态随机存取内存(DRAM)里面所储存的数据就需要周期性地更新。然而,当电力供应停止时,SRAM储存的数据还是会消失(被称为volatile memory),这与在断电后还能储存资料的ROM或闪存是不同的。

下图是存储器层次结构,以前在《深入理解计算机系统》这本书看到的,顺便放到此处:

2.数据结构

Cache中的存储空间往往是有限的,当Cache中的存储块被用完,而需要把新的数据载入到Cache的时候,就需要设计一种良好的算法来完成数据块的替换。LRU的思想是基于“最近用到的数据被重用的概率比较早用到的大的多”这个设计规则来实现的。

为了能够快速删除最久没有访问的数据项和插入最新的数据项,我们双向链表连接Cache中的数据项,并且保证链表维持数据项从最近访问到最旧访问的顺序。每次数据项被查询到时,都将此数据项移动到链表头部(O(1)的时间复杂度)。这样,在进行过多次查找操作后,最近被使用过的内容就向链表的头移动,而没有被使用的内容就向链表的后面移动。当需要替换时,链表最后的位置就是最近最少被使用的数据项,我们只需要将最新的数据项放在链表头部,当Cache满时,淘汰链表最后的位置就是了。

注:
对于双向链表的使用,基于两个考虑。首先是Cache中块的命中可能是随机的,和载入进来的顺序无关。其次,双向链表插入、删除很快,可以灵活的调整相互间的次序,时间复杂度为O(1)。

我们要访问某个结点,就需要顺序地一个个找,时间复杂度是O(n)。使用哈希表可以让我们在O(1)的时间找到想要访问的结点,或者返回未找到。

所以:LRU的典型实现是双向链表和哈希表,双向链表用于存储数据结点,并且它是按照结点最近被使用的时间来存储的。哈希表用于快速访问某个结点。

3.一个面试题

Question: Implement LRU cache algorithm 
Implement the LRU cache algorithm with the following interface: 
—————————————————————————————————
T get(K key);
void put(K key, T data);

此题参考价值还是蛮大的,不少公司,特别后台开发职位会考到此题。hawstein大牛用C++代码实现了,我们看下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
// A simple LRU cache written in C++
// Hash map + doubly linked list
#include <iostream>
#include <vector>
#include <ext/hash_map>
using namespace std;
using namespace __gnu_cxx;

template &lt;class K, class T&gt;
struct Node{
K key;
T data;
Node *prev, *next;
};

template &lt;class K, class T&gt;
class LRUCache{
public:
LRUCache(size_t size){
entries_ = new Node&lt;K,T&gt;[size];
for(int i=0; i< size; ++i)// 存储可用结点的地址
free_entries_.push_back(entries_+i);
head_ = new Node<K,T>;
tail_ = new Node<K,T>;
head_->prev = NULL;
head_->next = tail_;
tail_->prev = head_;
tail_->next = NULL;
}
~LRUCache(){
delete head_;
delete tail_;
delete[] entries_;
}
void Put(K key, T data){
Node<K,T> *node = hashmap_[key];
if(node){ // node exists
detach(node);
node->data = data;
attach(node);
}
else{
if(free_entries_.empty()){// 可用结点为空,即cache已满
node = tail_->prev;
detach(node);
hashmap_.erase(node->key);
}
else{
node = free_entries_.back();
free_entries_.pop_back();
}
node->key = key;
node->data = data;
hashmap_[key] = node;
attach(node);
}
}
T Get(K key){
Node<K,T> *node = hashmap_[key];
if(node){
detach(node);
attach(node);
return node->data;
}
else{// 如果cache中没有,返回T的默认值。与hashmap行为一致
return T();
}
}
private:
// 分离结点
void detach(Node<K,T>* node){
node->prev->next = node->next;
node->next->prev = node->prev;
}
// 将结点插入头部
void attach(Node<K,T>* node){
node->prev = head_;
node->next = head_->next;
head_->next = node;
node->next->prev = node;
}
private:
hash_map<K, Node<K,T>* > hashmap_;
vector<Node<K,T>* > free_entries_; // 存储可用结点的地址
Node<K,T> *head_, *tail_;
Node<K,T> *entries_; // 双向链表中的结点
};

int main(){
hash_map<int, int> map;
map[9]= 999;
cout<<map[9]<<endl;
cout<<map[10]<<endl;
LRUCache<int, string> lru_cache(100);
lru_cache.Put(1, "one");
cout<<lru_cache.Get(1)<<endl;
if(lru_cache.Get(2) == "")
lru_cache.Put(2, "two");
cout<<lru_cache.Get(2);
return 0;
}

看到此处,我就想研究下redis和memcache的cache是怎么设计的了。等下次研究完后,再做下笔记吧。

参考链接:

http://www.cs.uml.edu/~jlu1/doc/codes/lruCache.html

http://hawstein.com/posts/lru-cache-impl.html

http://blog.csdn.net/hexinuaa/article/details/6630384

 

linux内存空间分配和内存管理

前段时间把《程序员的自我修养–链接、装载与库》这本书看完了,期间有些地方不是太明白,当时网络搜索了下,今天把当时看到的东西摘抄下,基本文中大部分内容都是拷贝下面提到的参考链接里的东西,自己在组织下,以备以后需要时查看。

地址分为三类,逻辑地址,线性地址和物理地址。

在《深入理解linux内核》中其把地址分为三类:逻辑地址(汇编语言中操作数地址或指令的地址,对于80x86的CPU,逻辑地址是段+段内偏移地址)、线性地址(也叫虚拟地址)和物理地址。但在Stott Maxwell的《Linux Core Kernel Commentrary》中确是这样分的:逻辑地址(也叫虚拟地址)、线性地址和物理地址。按照386CPU总设计师John Crowford的解释,虚拟地址是保护模式下段和段内偏移量组成的地址,而逻辑地址就是代码段内偏移量,或称进程的逻辑地址。其实对于linux来说,这三种说法都没错,由于linux下并不主张将程序分段,而是主张分页,所以即使是在80x86的体系结构下,段的基地址也是0。因此逻辑地址、线性地址、虚拟地址在linux中其实是相同的。所以对于linux下的ELF可执行文件来说,代码段的起始地址0x08048000既是逻辑地址,也是线性地址也是虚拟地址。

1.X86的物理地址空间布局


以x86_32,4G RAM为例:

物理地址空间的顶部以下一段空间,被PCI设备的I/O内存映射占据,它们的大小和布局由PCI(Peripheral Component Interconnect,一种连接电子计算机主板和外部设备的总线标准)规范所决定。640K~1M这段地址空间被BIOS和VGA(Video Graphics Array,视频传输标准)适配器所占据。

由于这两段地址空间的存在,导致相应的RAM空间不能被CPU所寻址(当CPU访问该段地址时,北桥会自动将目的物理地址“路由”到相应的I/O设备上,不会发送给RAM),从而形成RAM空洞。

Linux内核是以物理页面(也称为PAGE FRAME)为单位管理物理内存的(X86机器中一个页面的大小默认为4KB),为了方便的记录每个物理页面的信息,Linux定义了page结构体,位于include/linux/mm_types.h
Linux系统在初始化时,会根据实际的物理内存的大小,为每个物理页面创建一个page对象,所有的page对象构成一个mem_map数组。进一步,针对不同的用途,Linux内核将所有的物理页面划分到3类内存管理区中,如图,分别为ZONE_DMA,ZONE_NORMAL,ZONE_HIGHMEM。

ZONE_DMA的范围是0~16M,该区域的物理页面专门供I/O设备的DMA(Direct Memory Access,直接内存存取)使用。之所以需要单独管理DMA的物理页面,是因为DMA使用物理地址访问内存,不经过MMU,并且需要连续的缓冲区,所以为了能够提供物理上连续的缓冲区,必须从物理地址空间专门划分一段区域用于DMA。

ZONE_NORMAL的范围是16M~896M,该区域的物理页面是内核能够直接使用的。

ZONE_HIGHMEM的范围是896M~结束,该区域即为高端内存,内核不能直接使用。

2.linux虚拟地址内核空间分布

在kernel image下面有16M的内核空间用于DMA操作。位于内核空间高端的128M地址主要由3部分组成,分别为vmalloc area,persistent kernel mapping(持久化内核映射区),tempoary kernel mapping(临时内核映射区)。

由于ZONE_NORMAL和内核线性空间存在直接映射关系,所以内核会将频繁使用的数据如kernel代码、GDT、IDT、PGD、mem_map数组等放在ZONENORMAL里。而将用户数据、页表(PT)等不常用数据放在ZONE HIGHMEM里,只在要访问这些数据时才建立映射关系(kmap())。比如,当内核要访问I/O设备存储空间时,就使用ioremap()将位于物理地址高端的mmio区内存映射到内核空间的vmalloc area中,在使用完之后便断开映射关系。

3.linux虚拟地址用户空间分布

用户进程的代码区一般从虚拟地址空间的0x08048000开始,这是为了便于检查空指针。代码区之上便是数据区,未初始化数据区,堆区,栈区,以及参数、全局环境变量。

4.linux虚拟地址与物理地址映射的关系 [内核访问物理地址]

Linux将4G的线性地址空间分为2部分,0~3G为user space,3G~4G为kernel space。

由于开启了分页机制,内核想要访问物理地址空间的话,必须先建立映射关系,然后通过虚拟地址来访问。为了能够访问所有的物理地址空间,就要将全部物理地址空间映射到1G的内核线性空间中,这显然不可能。于是,内核将0~896M的物理地址空间一对一映射到自己的线性地址空间中,这样它便可以随时访问ZONE_DMA和ZONE_NORMAL里的物理页面;此时内核剩下的128M线性地址空间不足以完全映射所有的ZONE_HIGHMEM,Linux采取了动态映射的方法,即按需的将ZONE_HIGHMEM里的物理页面映射到kernel space的最后128M线性地址空间里,使用完之后释放映射关系,以供其它物理页面映射。虽然这样存在效率的问题,但是内核毕竟可以正常的访问所有的物理地址空间了。

5.linux内存管理

Linux采用了分页的内存管理机制。由于x86体系的分页机制是基于分段机制的,因此,为了使用分页机制,分段机制是无法避免的。为了降低复杂性,Linux内核将所有段的基址都设为0,段限长设为4G,只是在段类型和段访问权限上有所区分,并且Linux内核和所有进程共享1个GDT(全局描述符表),不使用LDT(局部描述符表即系统中所有的段描述符都保存在同一个GDT中),这是为了应付CPU的分段机制所能做的最少工作。

Linux内存管理机制可以分为3个层次,从下而上依次为物理内存的管理、页表的管理、虚拟内存的管理。

当开启分段分页机制时,典型的x86寻址过程为:

内存寻址的工作是由Linux内核和MMU共同完成的,其中Linux内核负责cr3,gdtr等寄存器的设置,页表的维护,页面的管理,MMU则进行具体的映射工作。

物理内存主要包括:物理页面分配和物理页面回收,当空闲物理页面不足时,就需要从inactive_clean_list队列中选择某些物理页面插入空闲队列中,如果仍然不足,就需要把某些物理页面里的内容写回到磁盘交换文件里,腾出物理页面。

页表管理:

为了保持兼容性,Linux最多支持4级页表,而在x86上,实际只用了其中的2级页表,即PGD(页全局目录表)和PT(页表),中间的PUD和PMD所占的位长都是0,因此对于x86的MMU是不可见的。

6.linux中可执行程序与虚拟地址空间的映射关系

虚拟内存区域(VMA,Virtual Memory Area)是Linux中进程虚拟地址空间中的一个段,在Windows里面叫虚拟段。当操作系统创建线程后,会在进程相应的数据结构中设置一个.text段的VMA,它在虚拟空间中的地址为0x08048000~0x08049000,它对应ELF文件中的偏移为0的.text。由于linux下的ELF可执行文件会有很多个段(section),所以如果把每个section都映射为一个VMA,那么没有一个页大小的段(section)也会被映射为一个页的VMA,这样就浪费了物理空间,由于不足会用0补充。故ELF有一个装载的段(segment),与前面的段(section)不同,前面的段(section)主要用于链接,而段(segment)主要用于装载进内存。可以通过段(section)的权限来合并,如以代码段为代表的权限为可读可执行权限;以数据段和BSS段为代表的权限为可读可写的段;以只读数据为代表的权限为只读权限。

ELF与Linux进程虚拟空间映射关系如下:

7.可执行文件的装载过程

一、进程的建立

(1)创建一个独立的虚拟地址空间

(2)读取可执行文件头,并且建立虚拟空间与可执行文件的映射关系

(3)将CPU的指令寄存器设置成可执行文件的入口地址,启动运行。

二、页错误

上面的步骤执行完以后,其实可执行文件的真正指令和数据都没有载入到内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚存之间的映射关系而已。假设在上面的例子中,程序的入口地址为0X08048000,即刚好是.text段的起始地址。当CPU开始打算执行这个地址的指令时,发现0X08048000~0X08049000是个空白页,于是它就认为这是个页错误(Page Fault)。CPU将控制器交给操作系统,操作系统有专门的页错误处理例程来处理这种情况。这时候我们装载过程的第二步建立的数据结构起到了很关键的作用,操作系统将查询这个数据结构,然后找到空页面所在的VMA,计算出相应的页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,将进程中该虚拟页与分配的物理页之间建立映射关系,然后把控制权再还给进程,进程从刚才页错误的位置重新开始执行。随着进程的执行,页错误也会不断地产生,操作系统也会为进程分配相应的物理页面来满足进程执行的需求。当然有可能进程所需要的内存会超过可用的内存数量,特别是在有多个进程同时执行的时候,这时候操作系统就需要精心组织和分配物理内存,甚至有时候将分配给进程的物理内存暂时收回等等,这就涉及了操作系统的虚拟存储管理。

参考资料:

http://www.cnblogs.com/chengxuyuancc/archive/2013/04/17/3026920.html(绝大部分拷贝此链接资料)

http://www.cnblogs.com/zszmhd/archive/2012/08/29/2661461.html

《程序员的自我修养–装载、链接与库》

准备看下Redis的源代码

Redis是一个开源,BSD授权,高性能的key-value数据库,使用ANSI C编写。Redis的出现很大程度补偿了memcached这类key-value存储的不足,因为:

Memcached基本只支持简单的key-value存储,不支持枚举,不支持持久化和复制能功能。

Redis除key-value之外,还支持list,set,sorted set,hash等众多数据结构,提供了KEYS进行枚举操作,但不能在线上使用,如果需要枚举线上数据,Redis提供了工具可以直接扫描其dump文件,枚举出所有数据,Redis还同时提供了持久化和复制等功能。

如果简单地比较Redis与Memcached的区别,大多数都会得到以下观点:

  • Redis不仅仅支持简单的k/v类型的数据,同时还提供list,set,hash等数据结构的存储。
  • Redis支持数据的备份,即master-slave模式的数据备份。
  • Redis支持数据的持久化,可以将内存中的数据保持在磁盘中,重启的时候可以再次加载进行使用。
    Redis和memcached不同之处还在以下方面:1.网络IO模型 2.内存管理方面 3.数据一致性问题 4.存储方式及其它方面(上文提到了)等等,这里不在细说。

数据模型:

Redis 的外围由一个键、值映射的字典构成。与其他非关系型数据库主要不同在于:Redis 中值的类型不仅限于字符串,还支持如下抽象数据类型:
1.字符串列表
2.无序不重复的字符串集合
3.有序不重复的字符串集合
4.键、值都为字符串的哈希表
值的类型决定了值本身支持的操作。Redis 支持不同无序、有序的列表,无序、有序的集合间的交集、并集等高级服务器端原子操作。

持久化:

Redis 通常将全部的数据存储在内存中。2.4版本后可配置为使用虚拟内存,一部分数据集存储在硬盘上,但这个特性废弃了。
目前通过两种方式实现持久化:
使用快照,一种半持久耐用模式。不时的将数据集以异步方式从内存以 RDB 格式写入硬盘。
1.1版本开始使用更安全的 AOF 格式替代,一种只能追加的日志类型。将数据集修改操作记录起来。Redis 能够在后台对只可追加的记录作修改来避免无限增长的日志。

同步:

Redis支持主从同步。数据可以从主服务器向任意数量的从服务器上同步,从服务器可以是关联其他从服务器的主服务器。这使得 Redis 可执行单层树复制。从盘可以有意无意的对数据进行写操作。由于完全实现了发布/订阅机制,使得从数据库在任何地方同步树时,可订阅一个频道并接收主服务器完整的消息发布记录。同步对读取操作的可扩展性和数据冗余很有帮助。

性能:

当数据依赖不再需要,Redis 这种基于内存的性质,与在执行一个事务时将每个变化都写入硬盘的数据库系统相比就显得执行效率非常高。写与读操作速度没有明显差别。

上面东西,是从维基百科摘抄下来的,只是更好的了解此数据库。关于源码解读,发现有热心网友专门写了源码剖析,见下面链接:

Redis设计与实现

打算边看源码边看下此剖析。

Installation

Download, extract and compile Redis with:
$ wget http://redis.googlecode.com/files/redis-2.6.14.tar.gz $ tar xzf redis-2.6.14.tar.gz $ cd redis-2.6.14 $ make
The binaries that are now compiled are available in the src directory. Run Redis with:
$ src/redis-server

You can interact with Redis using the built-in client:
$ src/redis-cli redis&gt; set foo bar OK redis&gt; get foo "bar"

tutorial:
http://try.redis.io/

参考连接:

http://zh.wikipedia.org/wiki/Redis

http://www.redis.io/

http://simple-is-better.com/news/684

编译和调试C/C++程序常用到的一些知识点

做个笔记,记录下编译和调试C/C++程序常用的一些知识点,主要就是gcc、gdb的一些常用使用方法。

gcc

如图所示,先看下C/C++代码生成可执行文件的过程,共4步:预处理、编译、汇编、链接。

一步到位的编译命令为:

1
gcc -Wall -g test.c -o test

预处理

命令为:

1
gcc -E test.c -o test.i 或 gcc -E test.c

生成的test.i文件,可以通过vim或emacs直接打开。

gcc的-E选项,可以让编译器在预处理后停止,并输出预处理结果。比如预处理结果就是将stdio.h 文件中的内容插入到test.c中了。

编译为汇编代码(Compilation)

预处理之后,可直接对生成的test.i文件编译,生成汇编代码:

1
gcc -S test.i -o test.s

gcc的-S选项,表示在程序编译期间,在生成汇编代码后,停止,-o输出汇编代码文件。

汇编(Assembly)

对于上一小节中生成的汇编代码文件test.s,gas汇编器负责将其编译为目标文件,如下:

1
gcc -c test.s -o test.o

连接(Linking)

gcc连接器是gas提供的,负责将程序的目标文件与所需的所有附加的目标文件连接起来,最终生成可执行文件。附加的目标文件包括静态连接库和动态连接库。

对于上一小节中生成的test.o,将其与C标准输入输出库进行连接,最终生成程序test

1
gcc test.o -o test

一些技巧

生成的汇编代码可以使用vim或emacs直接打开,如下代码文件名为main.c。
x86系统:

1
gcc -S -O2 code.cpp

x64系统:

1
gcc -m32 -S -O2 code.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>

void f1(int *a)
{
for (int i = 0; i < 3;i++) {
a[i] = a[i] + 2;
}
}

void f2(int *a)
{
a[0] = a[0] + 2;
a[1] = a[1] + 2;
a[2] = a[2] + 2;
}

int main() {
int a[3] = {0,1,2};
f1(a);
f2(a);
return 0;
}

注意使用-O2编译,生成的.s文件可以直接打开,部分代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.LFB961:
.cfi_startproc
.cfi_personality 0x0,__gxx_personality_v0
pushl %ebp
.cfi_def_cfa_offset 8
.cfi_offset 5, -8
movl %esp, %ebp
.cfi_def_cfa_register 5
movl 8(%ebp), %eax
addl $2, (%eax)
addl $2, 4(%eax)
addl $2, 8(%eax)
popl %ebp
.cfi_restore 5
.cfi_def_cfa 4, 4
ret
.cfi_endproc
.LFE961:
.size _Z2f1Pi, .-_Z2f1Pi
.p2align 4,,15

objdump

最常用命令为:

1
objdump -S code

即同时显示源代码和汇编代码,部分代码为:

1
2
3
4
5
6
7
8
9
10
11
void f1(int *a)
{
for (int i = 0; i < 3;i++) {
a[i] = a[i] + 2;
400680: 83 07 02 addl $0x2,(%rdi)
400683: 83 47 04 02 addl $0x2,0x4(%rdi) // 取出寄存器的值,然后加上4,得到的值作为地址,间接寻址得到需要的数据
400687: 83 47 08 02 addl $0x2,0x8(%rdi)
}
}
40068b: c3 retq
40068c: 0f 1f 40 00 nopl 0x0(%rax)

二进制文件也可以使用vim或emacs打开,使用十六进制打开即可:

1
vim命令为:%!xxd

emacs命令为:

1
ALT+X hexl-mode

当然也可以使用hexdump查询十六进制(od也可以,只是默认八进制),命令为:

1
hexdump -C 二进制文件 | more

gdb

gdb有2种使用方式,一个是直接挂载指定的进程,命令为

1
gdb -p <pid>

另一种方式就加载二进制文件或core文件,命令为:

1
gdb -c core  得到二进制文件地址

加载二进制文件方式:

1
2
3
gdb binary 

gdb binary core

运行相关

1
2
3
4
5
6
7
8
9
10
11
run:简记为 r ,其作用是运行程序,当遇到断点后,程序会在断点处停止运行,等待用户输入下一步的命令。
continue (简写c ):继续执行,到下一个断点处(或运行结束)
next:(简写 n),单步跟踪程序,当遇到函数调用时,也不进入此函数体;此命令同 step 的主要区别是,step 遇到用户自定义的函数,将步进到函数中去运行,而 next 则直接调用函数,不会进入到函数体内。
step (简写s):单步调试如果有函数调用,则进入函数;与命令n不同,n是不进入调用的函数的
until:当你厌倦了在一个循环体内单步跟踪时,这个命令可以运行程序直到退出循环体。
until+行号: 运行至某行,不仅仅用来跳出循环
finish: 运行程序,直到当前函数完成返回,并打印函数返回时的堆栈地址和返回值及参数值等信息。
call 函数(参数):调用程序中可见的函数,并传递“参数”,如:call gdb_test(55)
frame/f 帧编号: 选择栈帧
start:开始执行程序,停在main函数第一行语句前面等待命令
quit:简记为 q ,退出gdb

break

1
2
3
4
5
6
7
8
b main           // main函数设置断点
b 8 // 第8行设置断点
b main.cpp:main //main文件的main函数设置断点
b main.cpp:8 //main文件的第8行设置断点
delete 断点号n //删除第n个断点
disable 断点号n //暂停第n个断点
enable 断点号n //开启第n个断点
delete breakpoints //清除所有断点

info

1
2
3
4
5
6
7
查看当前程序栈的信息: info frame----list general info about the frame
查看当前程序栈的参数: info args---lists arguments to the function
查看当前程序栈的局部变量: info locals---list variables stored in the frame
查看当前寄存器的值:info registers(不包括浮点寄存器) info all-registers(包括浮点寄存器)
查看当前栈帧中的异常处理器:info catch(exception handlers)
查看断点信息: info break
查看当前线程: info threads

查看源码

1
2
3
list :简记为 l ,其作用就是列出程序的源代码,默认每次显示10行。
list 行号:将显示当前文件以“行号”为中心的前后10行代码,如:list 12
list 函数名:将显示“函数名”所在函数的源代码,如:list main

打印

1
2
3
p/x var   //以十六进制打印整数var
p/a addr //打印十六进制形式的地址
p/u var //变量var无符号整数打印

分割窗口

1
2
3
4
5
6
layout:用于分割窗口,可以一边查看代码,一边测试
layout src:显示源代码窗口
layout asm:显示反汇编窗口
layout regs:显示源代码/反汇编和CPU寄存器窗口
layout split:显示源代码和反汇编窗口 //推荐这个命令,汇编的si/ni对应源码的s/n,si会进入汇编和C函数内部,ni不会
Ctrl + L:刷新窗口

多线程

1
2
3
info threads   //查看当前进程的线程
thread <id> //切换线程
f 3 //选择栈帧

参考链接:
http://www.cnblogs.com/ggjucheng/archive/2011/12/14/2287738.html
http://man.linuxde.net/objdump
http://linuxtools-rst.readthedocs.io/zh_CN/latest/tool/gdb.html

工欲善其事必先利其器

Emacs开发环境配置

Vim开发环境配置

首先,我们先看下自己的VIM都安装了什么插件,命令::scriptnames

我们先配置下vimrc文件,vim ~/.vimrc ,我们先设置让其显示行号和高亮代码,添加如下代码:

set nu! “显示行号
syntax enable “语法高亮
syntax on

TagList

功能:有点像VC里面的工作区,里面列出了当前文件的所有的宏,全局变量,函数名等。CTRL+W 连续2下可以左右切换。

下载taglist压缩包, 下载地址:http://www.vim.org/scripts/script.php?script_id=273,然后把解压的两个文件taglist.vim 和 taglist.txt 分别放到$HOME/.vim/plugin 和 $HOME/.vim/doc 目录中。

之后在~/.vimrc中添加如下几条命令:

let Tlist_Auto_Open = 1
let Tlist_Ctags_Cmd = ‘/usr/bin/ctags’
let Tlist_Show_One_File = 1
let Tlist_Exit_OnlyWindow = 1

此时,我们打开一个.c文件查看,发现左边多出一个workspace,当不想出现此工作区时,使用:Tlist可以关闭和打开。

Ctags

功能:ctags的作用是为系统头文件及自己的程序头文件建立索引,有了这个索引后,就可以使用其它VIM插件来实现相应的功能,比如我需要的功能就是代码提示,那就需要用omnicppcomplete插件,但该插件是依赖于ctags的。VIM默认已安装此插件。

sudo apt-get install exuberant-ctags

我们在源代码的最上层目录下使用此命令:

ctags -R –c++-kinds=+p 或者ctags -R –c-types=+p+x

再在VIM中运行此命令:

:set tags=/home/linuxer/source/tags 该命令将tags文件加入到vim中,也可以加入到~/.vimrc中。

使用方法:

我们把光标移动到函数上,按下CTRL+],VIM会自动切换到意义的函数处。返回时,我们输入CTRL+t。

vim“找到 tag: 1/? 或更多” 其他定义的查看方法:

:tselect 显示列表

:tn和:tp 显示后一个tag和前一个tag

或者g] 就可以了。

WinManager

功能:作用是一个文件管理器,能列出当前目标中的文件,可以通过这个浏览工程中的源文件。当光标停在某个文件或文件夹的时候,回车可以打开该文件或文件夹。

在说这个插件之前,我们先说下netrw.vim插件,这个插件在安装VIM时候就已经安装到系统里了,我们打开VIM输入:e /home/linuxer/source 就可以显示出该文件夹里面的文件,我们的插件其实原理就是由这个插件实现的。

使用方法:

http://www.vim.org/scripts/script.php?script_id=95 ,将对应的plugin和doc放入 ~/.vim 文件夹下对应的plugin和doc文件夹下。

在~/.vimrc下添加以下两行:

let g:winManagerWindowLayout=’FileExplorer|TagList’

或者 let g:winManagerWindowLayout=’FileExplorer’ “这2个显示方式不一样,读者选择自己喜欢的吧,一个是左右两列,一个是上下2列

nmap wm :WMToggle<cr>

在正常情况下输入wm(无:号)可以开启和打开,注意:第一种会把Taglist也关闭,此时用:Tlist可以重新打开。本人倾向第二种,使用时候用wm开启就可已了。

C/C++自动补全插件:clang complete

这个插件需要clang编译器的支持,我们先安装下:

sudo apt-get install clang

之后下载clang complete:http://www.vim.org/scripts/script.php?script_id=3302

方法:vim clang_complete.vmb -c ‘so %’ -c ‘q’

之后在~/.vimrc里添加 set completeopt=longest

配合CTRL+N函数、变量补全基本就差不多了。

上传了一份目前我使用的Vimrc配置到github,主要是为了方便你我使用,点击下面链接进入,我的Vimrc设置

人生大病,只是一“傲”字

先生曰:“人生大病,只是一傲字。为子而傲必不孝,为臣而傲必不忠,为父而傲必不慈,为友而傲必不信。故象与丹朱俱不肖,亦只一傲字,便结果了此生。诸君常要体此人心本是天然之理,精精明明,无致介染着,只是一无我而已:胸中切不可有,有即傲也。古先圣人许多好处,也只是无我而已,无我自能谦。谦者众善之基,傲者众恶之魁。”

引用王阳明先生的这段话来表示下我今后的学习和生活态度,与君共勉。