[전재] select와 epoll

9153 단어
최근 면접에서 셀렉트와 epoll의 효율이 떨어지는 이유를 물어본 친구가 있는데, 일반인과 마찬가지로 대부분 셀렉트는 윤문, epoll은 촉발식이어서 효율이 높다고 답했다.이 답안은 듣기에 매우 완벽하여 대체적으로 양자의 주요 차이점을 말하였다.
오늘은 한가로이 내부 코드를 뒤적여 내부 코드와 결합하여 여러분과 제 의견을 나눴습니다.
연결
나도 일찍이 프로젝트에서 select와 epoll을 사용한 적이 있다. select에 대해 가장 깊은 감명을 받은 것은 linux에서 select의 최대 수량 제한(windows에서 제한이 없는 것 같다)이다. 모든 프로세스의 select는 FD 를 가장 많이 처리할 수 있다.SETSIZE FD(파일 핸들),
1024개 이상의 핸들을 처리하려면 다중 프로세스를 사용할 수밖에 없습니다.
흔히 볼 수 있는 slect를 사용하는 다중 프로세스 모델은 다음과 같다. 하나의 프로세스 전문 accept가 성공한 후에 fd를 unix socket을 통해 하위 프로세스에 전달하고, 부 프로세스는 하위 프로세스에 따라 부하를 분배할 수 있다.부모 프로세스 1개 + 하위 프로세스가 4000개가 넘는 부하를 탑재한 적이 있습니다.
이런 모델은 우리 당시의 업무에서 매우 잘 운행되었다.epoll은 연결 수에 제한이 없습니다. 당연히 사용자가 API를 호출하여 설정 프로세스의 자원 제한을 재현해야 할 수도 있습니다.
2. 입출력 차이
1. select의 실현
이 단락은 linux 내장 코드와 결합하여 설명할 수 있다. 내가 사용한 것은 2.6.28이고, 다른 2.6의 코드는 차이가 많지 않을 것이다.
먼저 select:
select 시스템에서 호출된 코드는 fs/Select입니다.c하,
asmlinkage long sys_select(int n, fd_set __user *inp, fd_set __user *outp,
fd_set __user *exp, struct timeval __user *tvp)
{
struct timespec end_time, *to = NULL;
struct timeval tv;
int ret;

if (tvp) {
if (copy_from_user(&tv, tvp, sizeof(tv)))
return -EFAULT;

to = &end_time;
if (poll_select_set_timeout(to,
tv.tv_sec + (tv.tv_usec / USEC_PER_SEC),
(tv.tv_usec % USEC_PER_SEC) * NSEC_PER_USEC))
return -EINVAL;
}

ret = core_sys_select(n, inp, outp, exp, to);
ret = poll_select_copy_remaining(&end_time, tvp, 1, ret);

return ret;
}

앞에는 사용자 컨트롤에서 fd 복사하기set에서 내장 공간, 다음 구체적인 작업은coresys_select 중,
core_sys_select->do_select, 진정한 핵심 내용은do선택:
int do_select(int n, fd_set_bits *fds, struct timespec *end_time)
{
ktime_t expire, *to = NULL;
struct poll_wqueues table;
poll_table *wait;
int retval, i, timed_out = 0;
unsigned long slack = 0;

rcu_read_lock();
retval = max_select_fd(n, fds);
rcu_read_unlock();

if (retval < 0)
return retval;
n = retval;

poll_initwait(&table);
wait = &table.pt;
if (end_time && !end_time->tv_sec && !end_time->tv_nsec) {
wait = NULL;
timed_out = 1;
}

if (end_time && !timed_out)
slack = estimate_accuracy(end_time);

retval = 0;
for (;;) {
unsigned long *rinp, *routp, *rexp, *inp, *outp, *exp;

set_current_state(TASK_INTERRUPTIBLE);

inp = fds->in; outp = fds->out; exp = fds->ex;
rinp = fds->res_in; routp = fds->res_out; rexp = fds->res_ex;

for (i = 0; i < n; ++rinp, ++routp, ++rexp) {
unsigned long in, out, ex, all_bits, bit = 1, mask, j;
unsigned long res_in = 0, res_out = 0, res_ex = 0;
const struct file_operations *f_op = NULL;
struct file *file = NULL;

in = *inp++; out = *outp++; ex = *exp++;
all_bits = in | out | ex;
if (all_bits == 0) {
i += __NFDBITS;
continue;
}

for (j = 0; j < __NFDBITS; ++j, ++i, bit <<= 1) {
int fput_needed;
if (i >= n)
break;
if (!(bit & all_bits))
continue;
file = fget_light(i, &fput_needed);
if (file) {
f_op = file->f_op;
mask = DEFAULT_POLLMASK;
if (f_op && f_op->poll)
mask = (*f_op->poll)(file, retval ? NULL : wait);
fput_light(file, fput_needed);
if ((mask & POLLIN_SET) && (in & bit)) {
res_in |= bit;
retval++;
}
if ((mask & POLLOUT_SET) && (out & bit)) {
res_out |= bit;
retval++;
}
if ((mask & POLLEX_SET) && (ex & bit)) {
res_ex |= bit;
retval++;
}
}
}
if (res_in)
*rinp = res_in;
if (res_out)
*routp = res_out;
if (res_ex)
*rexp = res_ex;
cond_resched();
}
wait = NULL;
if (retval || timed_out || signal_pending(current))
break;
if (table.error) {
retval = table.error;
break;
}

/*
* If this is the first loop and we have a timeout
* given, then we convert to ktime_t and set the to
* pointer to the expiry value.
*/
if (end_time && !to) {
expire = timespec_to_ktime(*end_time);
to = &expire;
}

if (!schedule_hrtimeout_range(to, slack, HRTIMER_MODE_ABS))
timed_out = 1;
}
__set_current_state(TASK_RUNNING);

poll_freewait(&table);

return retval;
}

위의 코드는 매우 많은데, 사실 진정한 관건적인 코드는 이 구절이다.
mask = (*f_op->poll)(file, retval ? NULL : wait);
이것은 파일 시스템을 호출하는 폴 함수입니다. 서로 다른 파일 시스템의 폴 함수는 자연히 다르다. 우리가 주목하는 것은 tcp 연결이기 때문에 socketfs의 등록은net/socket에 있습니다.c리.
register_filesystem(&sock_fs_type);
socket 파일 시스템의 함수도 넷/Socket에 있습니다.c리:
static const struct file_operations socket_file_ops = {
.owner = THIS_MODULE,
.llseek = no_llseek,
.aio_read = sock_aio_read,
.aio_write = sock_aio_write,
.poll = sock_poll,
.unlocked_ioctl = sock_ioctl,
#ifdef CONFIG_COMPAT
.compat_ioctl = compat_sock_ioctl,
#endif
.mmap = sock_mmap,
.open = sock_no_open, /* special open code to disallow open via /proc */
.release = sock_close,
.fasync = sock_fasync,
.sendpage = sock_sendpage,
.splice_write = generic_splice_sendpage,
.splice_read = sock_splice_read,
};

sock폴을 따라가면,
마지막으로 넷/ipv4/tcp.c의
unsigned int tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
이것은 최종 검색 함수입니다.
즉, select의 핵심 기능은 tcp 파일 시스템의 poll 함수를 호출하여 끊임없이 조회하는 것이다. 원하는 데이터가 없으면 cpu를 계속 차지하지 않도록 스케줄링을 주동적으로 실행한다. 원하는 메시지가 연결될 때까지 스케줄링을 실행한다.
여기서 알 수 있듯이 select의 실행 방식은 기본적으로 서로 다른 폴을 호출하는 것이다. 필요한 소식이 있을 때까지 만약에 select 처리된 socket이 많다면 이것은 사실 전체 기계의 성능에도 소모이다.
2. epoll의 실현
epoll의 실현 코드는 fs/EventPoll에 있습니다.c하,
epoll은 몇 개의 시스템 호출과 관련되기 때문에 여기서 하나하나 분석하지 않고 몇 가지 관건만 분석한다.
첫 번째 포인트는요.
static int ep_insert(struct eventpoll *ep, struct epoll_event *event,
struct file *tfile, int fd)
이것은 우리가sys 를 호출하는 것이다epoll_ctl에서 socket을 관리할 때 호출되는 함수를 추가합니다. 관건적인 몇 줄은 다음과 같습니다.
epq.epi = epi;
init_poll_funcptr(&epq.pt, ep_ptable_queue_proc);
/*
* Attach the item to the poll hooks and get current event bits.
* We can safely use the file* here because its usage count has
* been increased by the caller of this function. Note that after
* this operation completes, the poll callback can start hitting
* the new item.
*/
revents = tfile->f_op->poll(tfile, &epq.pt);
여기도 파일 시스템을 호출하는 폴 함수입니다. 그러나 이번에는 하나의 구조를 초기화했습니다. 이 구조는 폴 함수의 콜백 함수를 가지고 있습니다. epptable_queue_proc,
poll 함수를 호출할 때 이 콜백을 실행합니다. 이 콜백의 기능은 현재 프로세스를 socket의 대기 프로세스에 추가하는 것입니다.
static void ep_ptable_queue_proc(struct file *file, wait_queue_head_t *whead,
poll_table *pt)
{
struct epitem *epi = ep_item_from_epqueue(pt);
struct eppoll_entry *pwq;

if (epi->nwait >= 0 && (pwq = kmem_cache_alloc(pwq_cache, GFP_KERNEL))) {
init_waitqueue_func_entry(&pwq->wait, ep_poll_callback);
pwq->whead = whead;
pwq->base = epi;
add_wait_queue(whead, &pwq->wait);
list_add_tail(&pwq->llink, &epi->pwqlist);
epi->nwait++;
} else {
/* We have to signal that an error occurred */
epi->nwait = -1;
}
}

매개 변수 whead가 실제로는 sk->sleep임을 알 수 있습니다. 현재 프로세스를 sk의 대기 대기열에 추가합니다. 이 socket이 데이터를 받거나 다른 이벤트가 터치될 때 호출됩니다.
sock_def_readable 또는 sockdef_write_space 알림 함수로 대기 프로세스를 깨웁니다. 이 두 함수는 socket이 생성될 때 sk 구조에 채워집니다.
앞에서 분석한 바와 같이 epoll은 확실히 select보다 똑똑하고 가볍기 때문에 더 이상 시끄럽게 질문할 필요가 없다.

좋은 웹페이지 즐겨찾기