I-O-Benchmark-On-Container/ContainerTracer

[trace-replay] io_submit()에서 비정상적인 메모리 접근

Closed this issue · 4 comments

io_submit() 함수가 실행될 때, 미리 설정된 buffer와 관련해서 발생하는 버그 같습니다.

buffer를 생성하는 부분(trace_replay.c:371)과
iocb에 해당 buffer를 설정하는 부분(trace_replay.c:392),
io_submit() 함수가 실행되는 부분(trace_replay.c:513)을 참고하여 디버깅해야 할 것 같습니다.

버그의 원인은 buffer가 가리키는 공간이 아닌 다른 공간을 io_submit()에서 접근해서 발생한 것 같습니다.

[valgrind log]
Syscall param io_submit(PWRITE) points to uninitialised byte(s)
    at 0x4E3C687: io_submit (in /lib/x86_64-linux-gnu/libaio.so.1.0.1)
    by 0x10B8AC: sub_worker (trace_replay.c:513)
    by 0x50456DA: start_thread (pthread_create.c:463)
    by 0x537EA3E: clone (clone.S:95)
  Address 0x5663000 is 0 bytes inside a block of size 4,096 alloc'd
    at 0x4C31E76: memalign (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    by 0x4C31F91: posix_memalign (in /usr/lib/valgrind/vgpreload_memcheck-amd64-linux.so)
    by 0x10A859: allocate_aligned_buffer (trace_replay.c:136)
    by 0x10B168: make_jobs (trace_replay.c:371)
    by 0x10B850: sub_worker (trace_replay.c:509)
    by 0x50456DA: start_thread (pthread_create.c:463)
    by 0x537EA3E: clone (clone.S:95)

[trace_replay.c:371]

...
359                 job = (struct io_job *)malloc(sizeof(struct io_job));
360                 align_sector(t_info, &blkno, &bcount);
361                 job->offset = (long long)blkno * SECTOR_SIZE;
362                 job->bytes = (size_t)bcount * SECTOR_SIZE;
363                 if (job->bytes > (size_t)MAX_BYTES)
364                         job->bytes = MAX_BYTES;
365 
366                 if (flags)
367                         job->rw = 1;
368                 else
369                         job->rw = 0;
370 
371                 job->buf = allocate_aligned_buffer(job->bytes);
...

[trace_replay.c:392]

...
387 #ifndef USE_RAND_BUF
388                 if (job->rw)
389                         io_prep_pread(&job->iocb, t_info->fd, job->buf,
390                                       job->bytes, job->offset);
391                 else
392                         io_prep_pwrite(&job->iocb, t_info->fd, job->buf,
393                                        job->bytes, job->offset);
394 #else
395                 if (job->rw)
396                         io_prep_pread(&job->iocb, t_info->fd, g_buf, job->bytes,
397                                       job->offset);
398                 else
399                         io_prep_pwrite(&job->iocb, t_info->fd, g_buf,
400                                        job->bytes, job->offset);
401
402 #endif
...

[trace_replay.c:513]

...
509                 cnt = make_jobs(t_info, ioq, jobq, max);
510                 if (!cnt && trace_eof(trace)) {
511                         goto Timeout;
512                 } else if (cnt > 0) {
513                         rc = io_submit(t_info->io_ctx, cnt, ioq);
514                         if (rc != cnt && rc < 0) {
515                                 int i;
516                                 for (i = 0; i < cnt; i++) {
517                                         free(jobq[i]->buf);
518                                         free(jobq[i]);
519                                 }
520                         }
521                         if (rc > 0)
522                                 t_info->queue_count += rc;
523                 }
...

링크랑 유사한 문제인 것 같네요.

해당 문제를 확인해본 결과 해결책은 찾았습니다만 해결이 불가능한 이슈입니다.

당장엔 실행에도 문제가 없으니 닫겠습니다.

추가로, 향후 이 이슈를 확인하실 분들을 위해서 제가 생각하는 해결 불가능의 이유를 설명 드리자면 아래와 같습니다.

이 문제를 해결을 하기 위해서는 복잡하게 이래저래 변경할 필요없이 "수정 전"의 내용을 "수정 후"와 같이 바꿔주면 됩니다.

수정 전

131 /* allocate a alignment-bytes aligned buffer */
132 void *allocate_aligned_buffer(size_t size)
133 {
134         void *p;
135
136         posix_memalign(&p, getpagesize(), size);
137
138         if (!p) {
139                 perror("memalign");
140                 exit(0);
141                 return NULL;
142         }
143
144         return p;
145 }

수정 후

131 /* allocate a alignment-bytes aligned buffer */
132 void *allocate_aligned_buffer(size_t size)
133 {
134         void *p;
135
136         posix_memalign(&p, getpagesize(), size);
137         memset(p, 0, size);
138
139         if (!p) {
140                 perror("memalign");
141                 exit(0);
142                 return NULL;
143         }
144
145         return p;
146 }

하지만 "수정 후"로 바꾸니 또 다른 문제가 발생했습니다.

trace-replay 특성상 많은 I/O 요청을 메모리에 적재해놓았다가 디스크에 요청을 하기 시작합니다. 여기서 적재를 위한 메모리의 확보 과정에서 posix_memalign을 사용하여 페이지 단위로 정렬된 메모리를 할당을 받습니다. 이것이 문제의 원인으로 보입니다.

먼저, 유닉스 계열은 아시다시피 CoW(Copy-on-Write)를 따릅니다. 그렇기 때문에 값을 변경하지만 않는다면 메모리를 일단은 할당을 받지 않습니다. 이는 posix_memalign도 동일할 것이라고 생각됩니다.

이런 가정 하에서 memset을 수행하게 되면 쓰기를 수행한 것이기에 메모리가 할당이 되게 되고, 이러면 어마어마한 양이 CoW의 혜택을 받지 못하고 할당이 되게 됩니다.

이렇게 할당 받은 메모리가 사용자의 물리적 메모리 크기를 넘게 되면 swap 영역으로 넘어가게 됩니다. 하지만 지금 할당에 사용하는 함수는 malloc이 아니라 posix_memalign이므로 swap이 벌어졌을 때, 후에 다시 사용하려고 디스크에서 가져오면 swap 정책에 따라서 성공할 수도 실패할 수도 있습니다.

단적으로, trace-replay를 "수정 후"와 같이 변경한 후에 가상 scsi 디스크를 만들어서 qemu 환경에서 수행해 본 결과 swap이 일어나는 순간 qemu가 Killed 되는 것을 확인할 수 있었습니다.

따라서, trace-replay의 근본적인 설계를 변경하지 않는 이상은 이를 해결하기란 어려울 것으로 생각됩니다.

아마 제 생각이 틀렸을 수도 있지만 지금으로는 이 문제의 해결책은 이게 최선이 아닐까 싶습니다.