본문 바로가기
개발이야기

무거운 프로그램에 대한 최적화 #sleep과 #select

by 코저씨 2021. 11. 28.
728x90

리눅스에서 c프로그램을 만들다 보면 실행하고 동작을 보고 테스트를 해보 끝을 내는 경우가 많다.

스펙대로 동작한다 단위 테스트를 해도 문제없었다 하면서 프로그램을 개발, 테스트를 하고 결국 문제가 없다 하면
배포가 되곤 한다. 하지만 “동작”이 잘되는 프로그램을 살펴보면 cpu사용량이 90%를 넘는 경우가 상당수이다.

외부장치의 키보드에서 키 입력을 기다리거나 특정 조건이 되면 처리를 하는 반복문 등에서 cpu를 쉬지 않고 계속 해당 루프를 돌게 해서 발생하는 문제다.

개발자 눈에는 소소의 각 함수가 천천히 돈다고 생각하지만 사실 임베디드 장치의 cpu라 해도 1초에 몇백만 번의 클럭이 발생하고 있고 함수는 몇만 번이 실행되고 있다. 그리고 cpu는 비명을 지르면서 개발자의 소스를 돌린다.

개발 초기라면 외부 입력에 대한 처리는 이벤트 처리방식으로 설계를 해서 리소스를 줄여야 하나, 오랜 시간 동안 유지 보수되던 소스를 처음부터 들어내는 건 불가능한 일이다. 그래서 기존 콘셉트를 유지하되 최적화를 하는 일이 필요하다.

오래된 소스들, 펌웨어처럼 개발한 애플리케이션들은 while루프 안에서 타임아웃, 특정 입력이 될 때까지 대기를 하는 구간이 많다.

//
while(1)
{
    if(timeout() == TRUE)
    {
    	break;
    }
    
    if(isInputKey() == KEY_1)
    {
    	break;
    }
    //
}

동작만 본다면 아무 이상이 없지만 CPU는 쉬지도 않고 일을 하게 된다. 배터리를 쓰는 기기에서 구동한다면 배터리 소모율이 증가하게 되고 다른 스레드도 동작하는 경우 리소스 선점 문제가 발생할 수가 있다. 


우선 해결방안에 대해 OS를 사용하는 애플리케이션 기준으로 알아보면,

 

1. Sleep() 함수 사용

Sleep(또는 usleep() 함수는 대부분 정확하지는 않지만 딜레이를 주기 위한 용도로 알려져 있다. 

하지만 현재 프로세스를 sleep상태로 변경하는 작업도 같이 하는데 (wake up은 alarm으로 발생). 이로써 현재 프로세스는 

대기상태에 빠지면서 CPU를 쉬게 해주는 역할을 한다. 

 

while(1){ }; 와 while(1) sleep(1); 를 실행해보면 CPU 리소스의 차이를 확인해 볼 수 있다. (다만 최적화 옵션은 OFF) 

다만 주의할 점이 커널의 CONFIG_HZ보다는 크게 해야 한다. 해당 값은 커널의 동작속도 (tick count)이며 이 값보다 작게 하면 큰 의미가 없다. 또 휴면 입력을 받는 경우 1~10ms의 딜레이는 크게 차이가 없으니 얼마나 sleep 값으로 할지는 결정을 잘해야 한다.

 

위 소스를 gcc -O0 test.c 로 빌드를 하면 top에서 cpu 사용량이 100%가 되는 것을 볼 수 있다. 계속 일을 하기 때문이다.

반면 while 함수에 sleep을 넣으면 현 프로세스가 일을 쉬기 때문에 리소스를 차지 안하게 된다.

워낙 cpu를 안써서 top에도 보이지 않는다.

 

2. Select() 함수 사용

 

select함수는 독특한 함수이며 이 함수는 다양한 목적으로 사용된다. 가장 큰 목적은 관리하고자 하는 파일 디스크립터 (fd) 관련해서 read 또는 write 등의 이벤트가 발생하는지 감지한다. 그리고 발생한 이벤트에 대해 처리를

여기서 많은 초보 개발자들이 그냥 "recv함수나 write함수를 실행하면 되잖아요?" 왜 소스를 길게 만드나요? 하고 문의를 한다.

 

recv함수의 flag를 0으로 사용하면 TCP데이터 수신이 있을 때까지 대기를 한다. 계속 수신을 대기하기에 CPU의 리소스를 사용하게 된다.

ssize_t recv(int sockfd, void *buf, size_t len, int flags);

그리고 데이터가 들어오면 recv함수는 수신 데이터 길이를 리턴하며 종료된다. 데이터의 수신이 발생한 상황에만 데이터를 가져오고 이 외에는 대기 또는 다른 일을 하려면 select 함수를 이용하면 된다.

 int select(int nfds, fd_set *restrict readfds,
                  fd_set *restrict writefds, fd_set *restrict exceptfds,
                  struct timeval *restrict timeout);

함수의 사용법은 https://man7.org/linux/man-pages/man2/select.2.html 링크를 참조하는 게 좋다. fd_set 구조체에 이벤트를 감지하고 싶은 파일 디스크립터를 설정하고 timeval 구조체로 시간을 설정하면 사용하면 select함수는 해당 파일 디스크립터의 이벤트가 발생하면 양수의 값이, 타임아웃이 발생하면 0 값, 통신에러 등 에러가 발생하면 음수의 값이 나온다.  또한 대기 중에는 리소스를 거의 쓰지 않기에 바로 동작하는 프로그램으로 설계가 가능해진다. 

 

 

3.c++ 이면 signal, slot을 이용할 것

내가 하는 프로젝트들은 거의 c언어 기반이다. 아직도 많은 서버 프로그램이나 펌웨어들은 c++ 보다는 c언어로 이루어져 있어 signal을  이용한 프로젝트는 없었다. 다만 위 기능들은 키, 터치 입력이나 파일 디스크립터 등의 이벤트가 발생했을 때, 실행할 함수를 지정해 실행해주는 일을 한다. 무식하게 루프 문을 반복하면서 읽을 필요는 없다는 뜻이다.

 

 

정리

각 사이트를 보면 sleep 함수에 대해 부정적인 의견도 있고. 긍정적인 의견도 있다. 부정적인 의견은 주로 OS가 스케줄링을 알아서 해주는데 괜한 참견을 하는 것이다라는 것과 멀티 플랫폼에서 sleep 함수가 정확한 함수가 아니다 보니 속도차이등이 난다는 의견이 있다. 

 

물론 처음부터 sleep 함수를 쓰는걸 기본으로 삼고 프로그래밍을 하는 것은 좋지 않다. 다만 회사 업무를 하다 보면 선배들이 만든 스파게티 소스를 유지보수하는 경우가 많기에 (참고로 대다수의 윗사람들은 멀쩡히 잘 돌아가는데 왜 바꾸나 하며 변화에 보수적이다.)  스파케티 소스를 개선하는 경우 sleep 함수는 그나마 고마운 함수가 된다. 

 

멀티플랫폼인 경우 sleep이 부담스러우면 select함수를 단순 타이머로 사용해도 된다. 파일 디스크럽터를 전부 0, NULL로 하고 timeval 구조체로 시간만 넣으면 이벤트 처리 없이 타임아웃만 처리하고 나오기에 정확한 타이머가 된다. 

 

결론은 프로그램을 처음부터 잘 설계하는 것도 중요하지만 이전의 프로그램을 유지하면서 개선하는 것도 능력이다.

 

 

 

728x90