1. epoll 原理

假设有 100 万用户同时与一个进程保持着 TCP 连接,而每一时刻只有几十个或几百个 TCP 连接时活跃的(接收到 TCP

包),也就是说,在每一时刻,进程只需要处理这 100 万连接中的一小部分连接。

select 和 poll 的做法是:进程每次收集事件的连接(其实这 100 万连接中的大部分都是没有事件发生的)都把这 100 万连

接的套接字传给操作系统(这首先就是用户态内存到内核态内存的大量复制),而由操作系统内核寻找这些连接上有没有未

处理的事件,将会是巨大的资源浪费,因此 select 和 poll 最多只能处理几千个并发连接。

而 epoll 则是在 Linux 内核中申请了一个简易的文件系统,把原先的一个 select 或者 poll 调用分成了 3 个部分:

  • 调用 epoll_create 建立 1 个 epoll 对象(在 epoll 文件系统中给这个句柄分配资源);
  • 调用 epoll_ctl 向 epoll 对象中添加这 100 万个连接的套接字;
  • 调用 epoll_wait 收集发生事件的连接。
Linux 2.6.35内核对 epoll 的实现

当某一个进程调用 epoll_create 方法时,Linux 内核会创建一个 eventpoll 结构体:

struct eventpoll {
...
/* 红黑树的根节点,这棵树中存储着所有添加到 epoll 中的事件,也就是这个 epoll 监控的事件 */
struct rb_root rbr; /* 双向链表 rdllist 保存着将要通过 epoll_wait 返回给用户的、满足条件的事件 */
struct list_head rdllist;
};

每一个 epoll 对象都有一个独立的 eventpoll 结构体,这个结构体会在内核空间中创造独立的内存,用于存储使用

epoll_ctl 方法向 epoll 对象中添加进来的事件。这些事件都会挂到 rbr 红黑树中,这样,重复添加的事件就可以通过红黑

树而高效地识别出来。

所有添加到 epoll 中的事件都会与设备(如网卡)驱动程序建立回调关系,即相应的事件发生时会调用 ep_poll_callback 回调

方法,它会把这样的事件放到上面的 rdllist 双向链表中。

在 epoll 中,对于每一个事件都会建立一个 epitem 结构体:

struct epitem {
...
/* 红黑树节点 */
struct rb_node rbn; /* 双向链表节点 */
struct list_head rdllink; /* 事件句柄等信息 */
struct epoll_filefd ffd; /* 指向所属的 eventpoll 对象 */
struct eventpoll *ep; /* 期待的事件类型 */
struct epoll_event event;
...
};

这里包含每一个事件对应着的信息。

当调用 epoll_wait 检查是否有事件的连接时,只是检查 eventpoll 对象中的 rdllist 双向链表是否有 epitem 元素,如果

rdllist 链表不为空,则把这里的事件复制到用户态内存中,同时将事件数量返回给用户。

2. epoll 的使用

2.1 epoll 的接口

2.1.1 epoll_create()

int epoll_create(int size);

系统调用 epoll_create() 创建一个 epoll 的句柄,之后 epoll 的使用都将依靠这个句柄来标识。参数 size 是告知 epoll 所

要处理的大致事件数目。不再使用 epoll 时,必须调用 close 关闭这个句柄。

2.1.2 epoll_ctl()

int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);

epoll_ctl 向 epoll 对象中添加、修改或者删除感兴趣的事件,返回 0 表示成功,否则返回 -1,此时需要根据 errno 错误码

判断错误类型。

  • epfd:是 epoll_create() 返回的句柄;
  • op:表示动作,可取的值有:
    • EPOLL_CTL_ADD: 添加新的事件到 epoll 中
    • EPOLL_CTL_MOD: 修改 epoll 中的事件
    • EPOLL_CTL_DEL:删除 epoll 中的事件
  • fd: 需要监听的描述符;
  • event: 是 epoll_event 结构体类型,用于告诉内核需要监听什么事件。epoll_event 的定义:
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
  • events:的取值有

    • EPOLLIN: 表示对应的连接上有数据可读(TCP 连接的远端主动关闭连接,也相当于可读事件,因为需要处理发送来的 FIN)
    • EPOLLOUT: 表示对应的连接上可以写入数据发送(主动向上游服务器发起非阻塞的 TCP 连接,连接建立成功的事件相当于可写事件)
    • EPOLLRDHUP:表示 TCP 连接的远端关闭或半关闭连接
    • EPOLLLPRI:表示对应的连接上有紧急数据需要读
    • EPOLLERR: 表示对应的连接发生错误
    • EPOLLHUP: 表示对应的连接被挂起
    • EPOLLLET: 表示将触发方式设置为边缘触发(ET),系统默认为水平触发(LT)
    • EPOLLONESHOT: 表示对这个事件只处理一次,下次需要处理时需重新加入 epoll
  • data:是一个 epoll_data 联合,定义如下:
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;

2.1.3 epoll_wait()

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);

收集在 epoll 监控的事件中已经发生的事件,如果 epoll 中没有任何一个事件发生,则最多等待 timeout 毫秒后返回。

epoll_wait 的返回值表示当前发生的事件个数,如果返回 0,则表示本次调用中没有事件发生,如果返回 -1,则表示发生错

误,需要检查 errno 判断错误类型。

  • epfd:是 epoll_create() 返回的句柄;
  • events:是分配好的 epoll_event 结构体数组,epoll 将会把发生的事件复制到 events 数组中(events 不可以是空指针,

    内核只负责把数据复制到这个 events 数组中,不会去帮助我们在用户态中分配内存)
  • maxevents:表示本次可以返回的最大事件数目,通常 maxevents 参数与预分配的 events 数组的大小是相等的;
  • timeout:表示在没有检测到事件发生时最多等待的时间(单位为毫秒),如果 timeout 为 0,则表示 epoll_wait 在

    rdllist 链表为空时,立刻返回,不会等待。

2.2 epoll 的工作模式

epoll 有两种工作模式:LT(水平触发)模式和 ET(边缘触发)模式。

默认情况下,epoll 采用 LT 模式工作,这时可以处理阻塞和非阻塞套接字。ET 模式的效率要比 LT 模式高,它只支持非阻塞

字。ET 和 LT 模式的区别在于,当一个新的事件到来时,ET 模式下当然可以从 epoll_wait调用中获取这个事件,可是如果这

次没有把这个事件对应的套接字缓冲区处理完,在这个套接字没有新的事件再次到来时,在 ET 模式下是无法再次从

epoll_wait 调用中获取这个事件的;而 LT 模式则相反,只要一个事件对应的套接字缓冲区还有数据,就总能从 epoll_wait

中获取这个事件。因此,LT 模式相对简单,而在 ET 模式下事件发生时,如果没有彻底地将缓冲区数据处理完,则会导致缓冲

区中的用户请求得不到响应。默认情况下,Nginx 是通过 ET 模式使用 epoll 的。

3. ngx_epoll_module 模块

3.1 配置项

static ngx_command_t  ngx_epoll_commands[] = {

    /* 在调用 epoll_wait 时,将由第 2 和第 3 个参数告诉 Linux 内核一次最多可返回多少个事件。这个配置项
* 表示调用一次 epoll_wait 时最多可以返回的事件数,当然,它也会预分配那么多 epoll_event 结构体用于
* 存储事件 */
{ ngx_string("epoll_events"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_epoll_conf_t, events),
NULL }, /* 指明在开启异步 I/O 且使用 io_setup 系统调用初始化异步 I/O 上下文环境时,初始分配的异步 I/O
* 事件个数 */
{ ngx_string("worker_aio_requests"),
NGX_EVENT_CONF|NGX_CONF_TAKE1,
ngx_conf_set_num_slot,
0,
offsetof(ngx_epoll_conf_t, aio_requests),
NULL }, ngx_null_command
};

存储配置项的配置结构体:

typedef struct {
ngx_uint_t events;
ngx_uint_t aio_requests;
} ngx_epoll_conf_t;

3.2 上下文结构体:ngx_epoll_module_ctx

static ngx_event_module_t  ngx_epoll_module_ctx = {
&epoll_name,
ngx_epoll_create_conf, /* create configuration */
ngx_epoll_init_conf, /* init configuration */ {
ngx_epoll_add_event, /* add an event */
ngx_epoll_del_event, /* delete an event */
ngx_epoll_add_event, /* enable an event */
ngx_epoll_del_event, /* disable an event */
ngx_epoll_add_connection, /* add an connection */
ngx_epoll_del_connection, /* delete an connection */
#if (NGX_HAVE_EVENTFD)
ngx_epoll_notify, /* trigger a notify */
#else
NULL, /* trigger a notify */
#endif
ngx_epoll_process_events, /* process the events */
ngx_epoll_init, /* init the events */
ngx_epoll_done, /* done the events */
}
};

3.2.1 ngx_epoll_init

static ngx_int_t ngx_epoll_init(ngx_cycle_t *cycle, ngx_msec_t timer)
{
ngx_epoll_conf_t *epcf; /* 获取配置项结构体 */
epcf = ngx_event_get_conf(cycle->conf_ctx, ngx_epoll_module); if (ep == -1)
{
/* 创建一个 epoll 句柄 */
ep = epoll_create(cycle->connection_n / 2); if (ep == -1)
{
ngx_log_error(NGX_LOG_EMERG, cycle->log, ngx_errno,
"epoll_create() failed");
return NGX_ERROR;
} #if (NGX_HAVE_EVENTFD)
if (ngx_epoll_notify_init(cycle->log) != NGX_OK) {
ngx_epoll_module_ctx.actions.notify = NULL;
}
#endif #if (NGX_HAVE_FILE_AIO)
ngx_epoll_aio_init(cycle, epcf);
#endif #if (NGX_HAVE_EPOLLRDHUP)
ngx_epoll_test_rdhup(cycle);
#endif
} if (nevents < epcf->events) {
if (event_list) {
ngx_free(event_list);
} /* 创建 event_list 数组,用于进行 epoll_wait 调用时传递内核态的事件 */
event_list = ngx_alloc(sizeof(struct epoll_event) * epcf->events,
cycle->log);
if (event_list == NULL) {
return NGX_ERROR;
}
} /* 将配置项epoll_events的参数赋给nevents */
nevents = epcf->events; /* 指明读写I/O的方法 */
ngx_io = ngx_os_io; /* 一旦设定Nginx使用某个事件处理模块,经过事件处理模块的初始化函数后,就把全局变量
* ngx_event_actions指向了该模块的封装 */
ngx_event_actions = ngx_epoll_module_ctx.actions; #if (NGX_HAVE_CLEAR_EVENT)
/* 默认是采用ET模式来使用epoll的,NGX_USE_CLEAR_EVENT宏实际上就是在告诉Nginx使用ET模式 */
ngx_event_flags = NGX_USE_CLEAR_EVENT
#else
ngx_event_flags = NGX_USE_LEVEL_EVENT
#endif
|NGX_USE_GREEDY_EVENT
|NGX_USE_EPOLL_EVENT; return NGX_OK;
}

3.2.2 ngx_epoll_add_event

static ngx_int_t ngx_epoll_add_event(ngx_event_t *ev, ngx_int_t event, ngx_uint_t flags)
{
int op;
uint32_t events, prev;
ngx_event_t *e;
ngx_connection_t *c;
struct epoll_event ee; /* 每个事件的 data 成员都存放着其对应的 ngx_connection_t 连接 */
c = ev->data; /* 下面会根据event参数确定当前事件时读事件还是写事件,
* 这会决定events是加上EPOLLIN标志还是EPOLLOUT标志位 */
events = (uint32_t) event; if (event == NGX_READ_EVENT) {
e = c->write;
prev = EPOLLOUT;
#if (NGX_READ_EVENT != EPOLLIN|EPOLLRDHUP)
events = EPOLLIN|EPOLLRDHUP;
#endif } else {
e = c->read;
prev = EPOLLIN|EPOLLRDHUP;
#if (NGX_WRITE_EVENT != EPOLLOUT)
events = EPOLLOUT;
#endif
} /* 根据active标志位确定是否为活跃事件,以决定到底是修改还是添加事件 */
if (e->active) {
op = EPOLL_CTL_MOD;
events |= prev; } else {
op = EPOLL_CTL_ADD;
} #if (NGX_HAVE_EPOLLEXCLUSIVE && NGX_HAVE_EPOLLRDHUP)
if (flags & NGX_EXCLUSIVE_EVENT) {
events &= ~EPOLLRDHUP;
}
#endif /* 加入flags参数到events标志位中 */
ee.events = events | (uint32_t) flags;
/* ptr成员存储的是ngx_connection_t连接 */
ee.data.ptr = (void *) ((uintptr_t) c | ev->instance); ngx_log_debug3(NGX_LOG_DEBUG_EVENT, ev->log, 0,
"epoll add event: fd:%d op:%d ev:%08XD",
c->fd, op, ee.events); /* 调用 epoll_ctl 方法向 epoll 中添加事件或者在 epoll 中修改事件 */
if (epoll_ctl(ep, op, c->fd, &ee) == -1) {
ngx_log_error(NGX_LOG_ALERT, ev->log, ngx_errno,
"epoll_ctl(%d, %d) failed", op, c->fd);
return NGX_ERROR;
} /* 将事件的 active 标志位置为 1,表示当前事件是活跃的 */
ev->active = 1;
#if 0
ev->oneshot = (flags & NGX_ONESHOT_EVENT) ? 1 : 0;
#endif return NGX_OK;
}

3.2.3 ngx_epoll_process_events

static ngx_int_t ngx_epoll_process_events(ngx_cycle_t *cycle, ngx_msec_t timer, ngx_uint_t flags)
{
int events;
uint32_t revents;
ngx_int_t instance, i;
ngx_uint_t level;
ngx_err_t err;
ngx_event_t *rev, *wev;
ngx_queue_t *queue;
ngx_connection_t *c; /* NGX_TIMER_INFINITE == INFTIM */ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll timer: %M", timer); /* 调用 epoll_wait 获取事件。注意,timer 参数是在 process_events 调用时传入的 */
events = epoll_wait(ep, event_list, (int) nevents, timer); err = (events == -1) ? ngx_errno : 0; /* 判断flags标志位是否指示要更新时间或ngx_event_timer_alarm是否为1 */
if (flags & NGX_UPDATE_TIME || ngx_event_timer_alarm) {
ngx_time_update();
} if (err) {
if (err == NGX_EINTR) { if (ngx_event_timer_alarm) {
ngx_event_timer_alarm = 0;
return NGX_OK;
} level = NGX_LOG_INFO; } else {
level = NGX_LOG_ALERT;
} ngx_log_error(level, cycle->log, err, "epoll_wait() failed");
return NGX_ERROR;
} if (events == 0) {
if (timer != NGX_TIMER_INFINITE) {
return NGX_OK;
} ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"epoll_wait() returned no events without timeout");
return NGX_ERROR;
} /* 遍历本次 epoll_wait 返回的所有事件 */
for (i = 0; i < events; i++) {
c = event_list[i].data.ptr; /* uintptr_t 在 64 位平台上为:typedef unsigned long int upintptr_t;
* 在 32 位平台上为:typedef unsigned int uintptr_t;
* 该类型主要是为了跨平台,其长度总是所在平台的位数,常用于存放地址. */ /* 将地址的最后一位取出来,用 instance 变量标识 */
instance = (uintptr_t) c & 1;
/* 无论是 32 位还是 64 位机器,其地址的最后 1 位肯定是 0,可以用下面这行语句把
* ngx_connection_t 的地址还原到真正的地址值 */
c = (ngx_connection_t *) ((uintptr_t) c & (uintptr_t) ~1); /* 取出读事件 */
rev = c->read; /* 判断这个读事件是否是过期事件 */
if (c->fd == -1 || rev->instance != instance) { /* 当 fd 套接字描述符为 -1 或者 instance 标志位不相等时,
* 表示这个事件已经过期了,不用处理 */ /*
* the stale event from a file descriptor
* that was just closed in this iteration
*/ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
} /* 取出事件类型 */
revents = event_list[i].events; ngx_log_debug3(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: fd:%d ev:%04XD d:%p",
c->fd, revents, event_list[i].data.ptr); /* 若事件发生错误或被挂起 */
if (revents & (EPOLLERR|EPOLLHUP)) {
ngx_log_debug2(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll_wait() error on fd:%d ev:%04XD",
c->fd, revents); /*
* if the error events were returned, add EPOLLIN and EPOLLOUT
* to handle the events at least in one active handler
*/ revents |= EPOLLIN|EPOLLOUT;
} #if 0
if (revents & ~(EPOLLIN|EPOLLOUT|EPOLLERR|EPOLLHUP)) {
ngx_log_error(NGX_LOG_ALERT, cycle->log, 0,
"strange epoll_wait() events fd:%d ev:%04XD",
c->fd, revents);
}
#endif /* 如果是读事件且该事件是活跃的 */
if ((revents & EPOLLIN) && rev->active) { #if (NGX_HAVE_EPOLLRDHUP)
if (revents & EPOLLRDHUP) {
rev->pending_eof = 1;
} rev->available = 1;
#endif rev->ready = 1; /* flags 参数中含有 NGX_POST_EVENTS 表示这批事件要延后处理 */
if (flags & NGX_POST_EVENTS) {
/* 如果要在 post 队列中延后处理该事件,首先要判断它是新连接事件还是普通事件,
* 以决定把它加入到 ngx_posted_accept_events 队列或者 ngx_posted_events 队列
* 中 */
queue = rev->accept ? &ngx_posted_accept_events
: &ngx_posted_events; /* 将这个事件添加到相应的延后执行队列中 */
ngx_post_event(rev, queue); } else {
/* 立即调用读事件的回调方法来处理这个事件 */
rev->handler(rev);
}
} /* 取出写事件 */
wev = c->write; /* 如果写事件且事件是活跃的 */
if ((revents & EPOLLOUT) && wev->active) { /* 检测该事件是否是过期的 */
if (c->fd == -1 || wev->instance != instance) { /* 如果 fd 描述符为 -1 或者 instance 标志位不相等时,表示这个
* 事件是过期的,不用处理 */
/*
* the stale event from a file descriptor
* that was just closed in this iteration
*/ ngx_log_debug1(NGX_LOG_DEBUG_EVENT, cycle->log, 0,
"epoll: stale event %p", c);
continue;
} wev->ready = 1;
#if (NGX_THREADS)
wev->complete = 1;
#endif /* 若该事件要延迟处理 */
if (flags & NGX_POST_EVENTS) {
/* 将该写事件添加到延迟执行队列中 */
ngx_post_event(wev, &ngx_posted_events); } else {
/* 立即调用这个写事件的回调方法来处理这个事件 */
wev->handler(wev);
}
}
} return NGX_OK;
}

ngx_epoll_process_events 方法会收集当前触发的所有事件,对于不需要加入到 post 队列延后处理的事件,该方法会立即执行

它们的回调方法,这其实是在做分发事件的工作,只是它会在自己的进程中调用这些回调方法而已,因此,每一个回调方法都不能

导致进程休眠或者消耗太多时间,以免 epoll 不能即时地处理其他事件.

什么是过期事件?

假设 epoll_wait 一次返回 3 个事件,在第 1 个事件的处理过程中,由于业务的需要,所以关闭了一个连接,而这个连接恰好对

应第 3 个事件。这样的话,在处理到第 3 个事件时,这个事件就已经是过期事件了,一旦处理必然出错。但是单纯把这个连接

的 fd 套接字置为 -1 是不能解决问题的。

如下场景:

假设第 3 个事件对应的 ngx_connection_t 连接中的 fd 套接字原先是 50,处理第 1 个事件时把这个连接的套接字关闭了,同

时置为 -1,并且调用 ngx_free_connection 将该连接归还给连接池。在 ngx_epoll_process_events 方法的循环中开始处理第 2

个事件,恰好第 2 个事件是建立新连接事件,调用 ngx_get_connection 从连接池中取出的连接非常可能就是刚刚释放的第 3 个

事件对应的连接。由于套接字 50 刚刚被释放,Linux 内核非常有可能把刚刚释放的套接字 50 又分配给新建立的连接。因此,

在循环中处理第 3 个事件时,这个事件就是过期的了。它对应的事件是关闭的连接,而不是新建立的连接。

因此,解决这个问题,依靠于 instance 标志位。当调用 ngx_get_connection 从连接池中获取一个新连接时,instance 标志位

就会置反。这样,当这个 ngx_connection_t 连接重复使用时,它的 instance 标志位一定是不同的。因此,在

ngx_epoll_process_events 方法中一旦判断 instance 发生了变化,就认为是过期事件而不予处理。

3.3 模块的定义

ngx_module_t  ngx_epoll_module = {
NGX_MODULE_V1,
&ngx_epoll_module_ctx, /* module context */
ngx_epoll_commands, /* module directives */
NGX_EVENT_MODULE, /* module type */
NULL, /* init master */
NULL, /* init module */
NULL, /* init process */
NULL, /* init thread */
NULL, /* exit thread */
NULL, /* exit process */
NULL, /* exit master */
NGX_MODULE_V1_PADDING
};

Nginx事件管理之epoll模块的更多相关文章

  1. Nginx事件管理之ngx_event_core_module模块

    1. 概述 ngx_event_core_module 模块是一个事件类型的模块,它在所有事件模块中的顺序是第一位.它主要完成以下两点任务: 创建连接池(包括读/写事件): 决定究竟使用哪些事件驱动机 ...

  2. Nginx事件管理之核心模块ngx_events_module

    1. ngx_events_module核心模块的功能介绍 ngx_events_module 模式是一个核心模块,它的功能如下: 定义新的事件类型 定义每个事件模块都需要实现的ngx_event_m ...

  3. Nginx事件管理机制-epoll

    epoll的最大好处在于他不会随着被监控描述符的数目的增长而导致效率极致下降. select是遍历扫描来判断每个描述符是否有事件发生,当监控的描述付越多时,时间消耗就越多,并且由于系统的限制selec ...

  4. Nginx事件管理之概念描述

    1. Nginx事件管理概述 首先,Nginx定义了一个核心模块ngx_events_module,这样在Nginx启动时会调用ngx_init_cycle方法解析配置项,一旦在 nginx.conf ...

  5. Nginx事件管理之定时器事件

    1. 缓存时间 1.1 管理 Nginx 中的每个进程都会单独地管理当前时间.ngx_time_t 结构体是缓存时间变量的类型: typedef struct { /* 格林威治时间1970年1月1日 ...

  6. Nginx源码分析--epoll模块

    Nginx采用epoll模块实现高并发的网络编程,现在对Nginx的epoll模块进行分析. 定义在src/event/modules/ngx_epoll_module.c中 1. epoll_cre ...

  7. Nginx事件管理之事件处理流程

    1. 概述 事件处理要解决的两个问题: "惊群" 问题,即多个 worker 子进程监听相同端口时,在 accept 建立新连接时会有争抢,引发不必要的上下文切换, 增加系统开销. ...

  8. Backbone事件管理——Backbone.Events模块API结构

    模块Backbone.Events的事件管理是通过Backbone提供的Events API来实现的,该API在1.0版本之前仅仅提供了几个基本的方法,如on.off.trigger.once分别执行 ...

  9. Nginx学习笔记1-Nginx功能模块以及进程管理

    1.         功能 1.1.           功能描述 使用缓存加速反向代理,简单负载均衡和容错: 使用缓存机制加速远程FastCGI服务器的访问: 模块化结构: 基本的HTTP功能: 邮 ...

随机推荐

  1. Struts的相关基础

    为什么要用struts? 1.该框架基予mvc的开发设计模式上的,所以拥有mvc的全部优点,他在M.V.C上都有涉及,但它主要是提供一个好的控制器和一套定制的标签库上,有mvc的一系列优点:层次分明, ...

  2. CentOS7安装Docker-CE并部署项目

    前言 这是我第一次使用dokcer部署项目,现学现卖.成功之后把所有用到的安装及部署和操作命令做一个总结.如有不足,请指教. 使用的是阿里云服务器.CentOS7版本. Dokcer安装 1.Cent ...

  3. SVN主从备份

    SVN主从备份 两套环境:192.168.67.63(主SVN) 192.168.67.60(从SVN) 1.主环境上已经装好SVN并且存在数据仓库/home/svndata在从环境上,新建一/hom ...

  4. tomcat启动程序乱码和tomcat启动程序的标题乱码处理

    启动程序运行中的文字乱码: 解决方案: 找到Tomcat目录下conf文件夹中的logging.properties文件, 打开logging.properties文件,找到文件中的java.util ...

  5. 深入学习Mybatis框架(二)- 进阶

    1.动态SQL 1.1 什么是动态SQL? 动态SQL就是通过传入的参数不一样,可以组成不同结构的SQL语句. 这种可以根据参数的条件而改变SQL结构的SQL语句,我们称为动态SQL语句.使用动态SQ ...

  6. centos7 搭建pxe 安装centos windows(非全自动)(这个教程测试centos6和7.2可以用,Windows各版本也可以)

    yum install dhcp xinetd syslinux tftp-server httpd 编辑dhcpdb配置(192.168.0.1为本机IP) ; max-lease-time ; l ...

  7. P3806 离线多次询问 树上距离为K的点对是否存在 点分治

    询问树上距离为k的点对是否存在 直接n^2暴力处理点对 桶排记录 可以过 #include<cstdio> #include<cstring> #include<algo ...

  8. Linux命令行——scp命令

     原创声明:本文系博主原创文章,转载或引用请注明出处. scp 一般格式: scp [option] src dst 1. src和dst格式为: [user@]host:/path/to/file ...

  9. python3安装模块,摘自网上

    配置好Python3.6和pip3安装EPEL和IUS软件源 yum install epel-release -y yum install https://centos7.iuscommunity. ...

  10. 报错:required string parameter XXX is not present

    报错:required string parameter XXX is not present 不同工具发起的get/delete请求,大多数不支持@RequestParam,只支持@PathVari ...