새 업무로 안드로이드 프레임워크를 맡게 되었다. 안드로이드 MediaCodec 관련 내용을 분석하며 다양한 비동기 처리 방식(Amessage, AHandler, ALooper, hwBinder) 등이 있다는 것을 알게 되었고, 내부 구현이 어떻게 처리되는지 궁금증이 생겼다.
이를 위해 다중 FIFO io 예제를 만들고, select와 poll, epoll가 어떻게 효율적으로 활용될 수 있는지를 분석한다.
이번 포스트에서는 select에 대해서 심도 높게 분석해본다.
Everything is file!
리눅스 환경에서 모든 것은 파일이다. 하드웨어, 디바이스 드라이버, 디렉토리, 소켓 등 모든 요소를 파일로 추상화하여 관리한다.
파일 디스크립터는 각 프로세스가 파일을 다루는 방식이다. 우리가 코드에서 어떤 파일을 열었을 때, 운영체제는 프로세스에게 해당 파일을 지칭하는 index 값을 리턴해준다. 이를 file descriptor라고 한다.
일반적으로 0은 standard input, 1은 standard output, 2는 error를 나타낸다.
File dscriptor를 이해하기 위해서는 다음과 같은 정보를 알아야 한다.
File Descriptor Table
file discriptor table은 모든 파일 디스크립터를 가지고 있는 테이블이다. 파일 디스크립터 인덱스를 키로하여 파일 정보(File Table Entry)를 얻을 수 있다.
File Table Entry
파일 테이블 엔트리는 열려있는 파일에 대한 메모리 내부의 대리자 역할을 하는 구조체이다. 프로세스가 open system call을 수행할 시 발생한다.
select와 poll, epoll의 예제 구현을 위하여 우선 FIFO를 통한 프로세스 간 통신 베이스 코드를 작성하였다.
Figure 1. 다중 파이프 구조
Figure 1은 만들 예제의 구조를 보여준다.
// client.cpp
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int fd;
// make multiple PIPE
char *pipes[3] = {"/tmp/PIPE1", "/tmp/PIPE2", "/tmp/PIPE3"};
for (int i = 0; i < 3; i++) mkfifo(pipes[i], 0666);
char buf[30];
for (int i = 0; i < 3; i++)
{
int fd = open(pipes[i], O_WRONLY);
sprintf(buf, "client write data to %d\n", i);
write(fd, buf, sizeof(buf));
}
return 0;
}
client는 3개의 named pipe를 통해 통신할 것이다. 그리고 순차적으로 서버와 소통 할 것이다.
//server.cpp
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int fd;
// make multiple PIPE
char *pipes[3] = {"/tmp/PIPE1", "/tmp/PIPE2", "/tmp/PIPE3"};
for (int i = 0; i < 3; i++)
mkfifo(pipes[i], 0666);
char buf[30];
for (int i = 0; i < 3; i++)
{
int fd = open(pipes[i], O_RDONLY);
read(fd, buf, sizeof(buf));
printf("%d pipe input: %s", i+1, buf);
}
return 0;
}
server는 각 pipe를 만들고, 순서대로 통신하여 값을 읽고, 그대로 출력할 것이다.
Figure 2. 결과
client 코드를 다음과 같이 수정해보자.
// client.cpp
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main(int argc, char** argv)
{
if(argc < 2) {
printf("need to pipe index\n");
return 0;
}
int idx = argv[1][0] - '0';
char pipe[15];
char buf[30];
sprintf(pipe, "/tmp/PIPE%d", idx);
int fd = open(pipe, O_WRONLY);
sprintf(buf, "client write data to %d\n", idx);
write(fd, buf, sizeof(buf));
return 0;
}
이제 client는 argment로 index를 받아 해당 파이프로 통신한다. 만약, ./client 1 ./client 2, ./client 3 순으로 호출할 경우, 이전과 동일하게 동작한다.
하지만 순서를 바꿔 ./client 2를 먼저 호출해보자. 이 경우, client2의 write는 server가 read 할 때 까지 block 될 것이고, server는 pipe1을 읽기 위하여 block 상태에 있으므로 pipe1이 처리될 때 까지, client2와 server는 block 상태를 유지할 것이다.
이를 효율적으로 처리하기 위하여 select, poll, epoll을 이용할 수 있다.
select는 블록 환경에서 비동기적으로 다중 입출력을 처리하기 위하여 고안되었다. 위와 같이 블록이 되는 명령을 순차적으로 처리할 경우, 먼저 처리된 다른 PIPE의 입력의 처리가 지연될 수 있다. 위의 코드가 잘 동작할 수 있도록, select 함수를 이용하여 server 코드를 수정해보자.
//server.cpp
#include <stdio.h>
#include <string.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
// make multiple PIPE
char *pipes[3] = {"/tmp/PIPE1", "/tmp/PIPE2", "/tmp/PIPE3"};
for (int i = 0; i < 3; i++)
mkfifo(pipes[i], 0666);
char buf[30];
fd_set readfds;
FD_ZERO(&readfds);
int fd[3];
while (true)
{
for(int i=0;i<3;i++) {
fd[i] = open(pipes[i], O_RDONLY | O_NONBLOCK);
FD_SET(fd[i], &readfds);
}
int state = select(fd[2]+1, &readfds, NULL, NULL, NULL);
if(state==-1) break;
else if(state == 0) continue;
for(int i=0;i<3;i++){
if(fd[i]==-1) continue;
if(FD_ISSET(fd[i], &readfds)){
read(fd[i], buf, sizeof(buf));
printf("%d pipe input: %s", i+1, buf);
}
}
for(int i=0;i<3;i++) close(fd[i]);
}
return 0;
}
수정된 코드는 select 함수를 사용한다. select를 이용한 블로킹 처리는 다음과 같은 함수를 통하여 구현할 수 있다.
select(nfds, readfds, writefds, errorfds, time)
// nfds: 모니터링 할 fd의 최대값 +1이 된다. +1인 이유는 마지막 비트는 가장 최근에 사용된 fd를 나타낸다.
// readfds, writefds, errorfds => 모니터링 할 fd_set, NULL 일 경우 모니터링 하지 않음
// time: time 만큼 block된다.
// return: -1일 경우 에러, 0일 경우 대기 중, 0이 아닌 값일 경우 이벤트 발생
FD_SET(fd, fd_set)
//fd의 상태를 fd_set에 기록한다.
FD_ISSET(fd, fd_set)
//fd_set에서 fd가 트루인지 확인
FD_ZERO(fd_set)
// fd_set의 값을 0으로 초기화
select는 지정된 시간 동안만 '블록'될 수 있다. 원하는 감시 대상(파일 디스크립터)를 지정하고(FD_SET), 해당 파일이 읽을 수 있는 상태가 되는지 조사한다. 즉, 다음과 같은 상태를 기다린다.
즉, 이 경우, READ가 가능한 fd가 하나 이상 존재할 때, select는 양의 정수 값을 리턴한다. 이를 통해 ./client 2 => ./client 1 => ./client 3 순으로 호출해도 순서에 따라 처리할 수 있다.
Figure 3. 2->1->3 순 결과
select는 0부터 select에 지정된 fd+1까지 모든 비트를 선형적으로 검사한다.
세부구현을 보자.(do_select함수) https://elixir.bootlin.com/linux/latest/source/fs/select.c#L476
select는 system call이므로, 작동 함수명은 sys_select이다. "SYSCALL_DEFINE5(select" 로 구글에 검색해보면 시스템 콜 구현 내용을 볼 수 있다. 여기서, SYSCALL_DEFINE(X)의 X는 select의 파라미터 수이다.
세부 동작 방식은 다음과 같다.
busy wait 방식의 select 메카니즘을 통해 다중 입출력을 효율적으로 처리할 수 있는 방법에 대해서 스터디하였다.
https://pangtrue.tistory.com/31
http://egloos.zum.com/furmuwon/v/11127264
https://elixir.bootlin.com/linux/2.2.3pre3/source/fs/select.c#L61