리눅스 기반에서의 다중 접속 서버 구현 방법
ps -u 커맨드를 사용하면 확인 가능 (1은 init 프로세스에 할당됨)
[실행 결과]
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
// 성공 시 프로세스 ID, 실패 시 -1을 리턴
// 호출한 프로세스의 복사본 프로세스를 생성함
// 성공 시
// (1) 원본 프로세스(부모 프로세스)와 복사본 프로세스(자식 프로세스)에게 전달되는 리턴 값이 달라짐
// (2) 메모리 공간(데이터, 힙, 스택)을 그대로 복사함 -> 부모 프로세스와 자식 프로세스 간의 데이터 공유가 일어나지 않음
// fork.c
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char** argv)
{
pid_t pid;
int data = 10;
pid = fork();
if(pid == -1)
printf("fork 실패, 프로세스 id : %d \n", pid);
else
printf("fork 성공, 프로세스 id : %d \n", pid);
if(pid == 0) // 자식 프로세스라면 (리턴값이 0이라면)
data += 10;
else // 부모 프로세스라면
data -= 10;
printf("data : %d \n", data);
return 0;
}
[실행 결과]
[좀비 프로세스]
프로세스가 생성되고 나서 할 일을 다 했음에도 불구하고 사라지지 않고 중요한 리소스를 차지해서 성능을 저하시키는 원인이 되는 프로세스
[좀비 프로세스가 생성되는 이유]
fork 함수의 호출로 자식 프로세스가 생성이 되고 그 자식 프로세스가 exit 함수나 return 문을 이용하여 값을 반환하는 경우
반환된 값은 커널로 넘어감
커널은 자식 프로세스의 실행이 끝났더라도 리턴 값을 부모 프로세스에게 넘겨줄 때까지 자식 프로세스를 소멸시키지 않음
⇒ 리턴 값이 부모 프로세스에게 전달되도록 해야 자식 프로세스가 소멸되지 않고 좀비 프로세스가 되는 것을 막을 수 있음
커널은 부모 프로세스가 가만히 있는데 자식 프로세스의 리턴 값을 전달해 주지는 않음
⇒ 부모 프로세스가 커널에게 자식 프로세스가 리턴한 값을 전달해 달라고 요청해야 함
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main(int argc, char** argv)
{
pid_t pid;
int data = 10;
pid = fork(); // 자식 프로세스 생성
if(pid < 0)
data += 10; // 자식 프로세스에 의해 실행됨
else
{
data -= 10; // 부모 프로세스에 의해 실행됨
sleep(20); // 20초 동안 대기 상태로 둔 이유는 그 사이에 자식 프로세스가 좀비가 된 것을 확인해 보기 위해
}
print("data : %d \n", data);
return 0;
}
[실행 결과]
ps -u가 실행되기 전 자식 프로세스는 0을 리턴하면서 종료하게 되고, 부모 프로세스는 20초간의 정지 상태에 들어가게 됨
부모 프로세스가 자식 프로세스의 리턴 값을 읽어 들이지 않았으므로 자식 프로세스는 좀비 프로세스가 됨
자식 프로세스가 소멸되기 위해서는 부모 프로세스가 자식 프로세스의 리턴 값을 읽어 들여야 함
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int* status)
// 성공 시 종료된 자식 프로세스 ID 실패 시 -1 리턴
// wait 함수가 호출되었을 때, 이미 종료 된 자식 프로세스가 있다면,
// 그 프로세스가 리턴한 값을 함수 호출 시 전달되는 포인터를 통해 읽어들임
// 그러나 wait 함수가 호출된 시점에서 종료된 자식이 없다면 임의의 자식 프로세스가 종료될 때 까지
// 블로킹 상태에 놓이게 되며, 자식 프로세스 중 하나가 종료되어야만 리턴 값을 읽어 들이고 빠져 나오게 됨
status 포인터가 가리키는 변수에 저장된 값을 통해서 원하는 정보만 리턴받을 수 있도록 구현되어 있는 매크로 함수들
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char** argv)
{
pid_t pid, child;
int data = 10;
int state;
pid = fork(); // 자식 프로세스 생성
if(pid < 0)
printf("fork 실패 프로세스 id : %d \n", pid);
printf("fork 성공 프로세스 id : %d \n", pid);
if(pid == 0)
data += 10;
else
{
// 부모 프로세스에 의해서 실행됨
data -= 10;
child = wait(&state); // wait 함수를 호출하면서 자식 프로세스가 종료되기를 기다림
printf("자식 프로세스 ID = %d \n", child);
printf("리턴 값 = %d \n", WEXITSTATUS(state));
sleep(20); // 20초 동안 정지 상태에 들어갔을 때는 종료된 자식 프로세스가 소멸된 상태임 -> 프로세스 상태 확인을 위해 대기 시킴
}
printf("data : %d \n", data);
return 0;
}
[실행 결과]
20초 정지 상태에 있을 때 프로세스의 상태를 확인 해 보면 자식 프로세스에 대한 정보가 없음
⇒ 부모 프로세스가 생성했던 자식 프로세스가 완전히 사라짐
wait 함수는 경우에 따라서 적절하지 못할 수 있음
→ wait 함수를 호출한 시점에서 종료된 자식 프로세스가 존재하지 않으면 종료된 자식 프로세스가 생길 때 까지 무한 블로킹 상태에 있게 되기 때문
⇒ 좀비가 존재하는지 확신할 수 없는 상태에서 호출했을 경우 문제가 되기도 함
기본적인 동작은 wait 함수와 동일하지만, 함수 호출 시 전달되는 인자에 따라서 블로킹 문제를 해결해 주는 함수가 있음
#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int* status, int options)
// 성공 시 종료된 자식 프로세스 ID (경우에 따라 0), 실패 시 -1 리턴
// pid : 종료 확인을 원하는 자식 프로세스의 ID
// status : wait 함수의 status와 같은 역할을 함
// options : sys/wait.h에 정의되어 있는 'WNOHANG' 상수를 인자로 전달하게 되면 이미 종료된 자식 프로세스가 없는 경우
// 대기 상태로 들어가지 않고 바로 리턴하게 됨 -> 무한정 블로킹 상태에 빠지지 않음 => 이 때 리턴되는 값이 0
#include <stdio.h>
#include <sys/wait.h>
#include <sys/types.h>
int main(int argc, char** argv)
{
pid_t pid, child;
int data = 10;
int state;
pid = fork();
if(pid < 0)
printf("fork 실패, 프로세스 id : %d \n", pid);
printf("fork 성공, 프로세스 id : %d \n", pid);
if(pid == 0)
{
data += 10;
sleep(10); // 자식 프로세스의 종료를 지연시키기 위해 10초간 정지 상태에 들어감
}
else
{
data -= 10;
// 3초 간격으로 waitpid 함수를 호출하면서, 자식 프로세스의 소멸을 확인함
// 종료된 자식 프로세스가 없다면 계속해서 do ~ while문을 반복하게 됨
do
{
sleep(3);
puts("3초 대기");
child = waitpid(-1, &state, WNOHANG);
}while(chiild == 0);
printf("Child process id = %d, return value = %d \n\n", child, WEXITSTATUS(state));
}
printf("data : %d \n", data);
return 0;
}
[실행 결과]
3초 대기라는 메시지가 출력되고 있음
→ waitpid 함수 호출 시 종료된 자식 프로세스가 없다면, 바로 0을 리턴하면서 함수를 빠져 나온다는 것을 보여줌
프로세스를 생성하는 방법과 완전히 소멸하는 방법에 대해서 알았지만 waitpid 함수를 언제 호출할건지에 대한 문제가 남아있음
블로킹 문제가 해결된다 하더라도, 이전 예제처럼 계속해서 루프를 돌면서 확인해 볼 수는 없음
→ 가장 이상적인 방법은 자식 프로세스가 종료되는 순간에 커널이 부모 프로세스에게 알려줘서 이를 처리하는 것임
⇒ 시그널 핸들링을 사용하면 이런 구현이 가능함
(참고)
이처럼 커널에 의해서 메시지가 전달되는 방식을 비동기(Asynchronous) 방식이라고 함
[특정 상황과 시그널]
#include <signal.h>
void (*signal(int signum, void (*func)(int)))(int);
// signum : 프로세스가 가로채고자 하는 시그널 상수
// 프로세스가 가로챈다의 의미는
// 특정 상황이 발생하여 운영체제가 이를 알리기 위해서 시그널을 프로세스에게 전달하는 것
// func : 시그널을 처리할 함수의 포인터
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig);
int main(int argc, char** argv)
{
int state;
int num = 0;
// 시그널이 발생하는 경우 handler 함수가 호출되도록 설정
signal(SIGINT, handler);
while(1)
{
printf("%d : 대기중 \n", num++);
sleep(2);
if(num > 5) break;
}
return 0;
}
void handler(int sig)
{
signal(SIGINT, handler);
printf("전달된 시그널은 %d \n", sig);
}
[실행 결과]
실행 중 Ctrl + C(인터럽트 발생)를 누르면 프로그램에서 정의한 시그널 핸들러가 실행되면서 메시지를 콘솔에 출력함
SIGINT 시그널에 대해 핸들러 설정을 해 주지 않았다면 SIGINT 시그널이 발생하지 않을까?
→ signal 함수의 호출을 주석 처리하고 실행하면 Ctrl + C 키를 누르자 마자 프로그램이 바로 종료됨
즉, 핸들러를 설정해 주지 않았다고 해서 시그널이 발생하지 않는 것은 아님
시그널은 발생됨
→ 우리가 직접 핸들러를 설정해 주지 않으면, 운영체제 차원에서 지니고 있는 기본적인 핸들러가 동작하게 됨
⇒ 운영체제에서 디폴트로 제공하는 SIGINT 시그널에 대한 핸들러는 프로세스를 그냥 종료 시켜버림
(핸들러를 설정 해 주면 운영체제 핸들러 대신 내가 설정한 시그널 핸들러가 호출됨)
#include <signal.h>
int sigaction(int signum, const struct sigaction* act, struct sigaction* oldact);
// 리턴 값은 성공 시 0을, 실패 시 -1을 리턴함
// signum : signal 함수와 마찬가지로 가로 채고자 하는 시그널의 종류를 인자로 전달함
// act : 새로 등록할 시그널 핸들러 정보로 초기화된 sigaction 구조체 변수의 포인터를 인자로 전달함
// oldact : 이전에 등록되었던 시그널 핸들러의 포인터를 얻고자 할 때 사용함
[sigaction 구조체]
struct sigaction
{
void (*sa_handler)(int)
sigset_t sa_mask;
int sa_flags;
}
// sa_handler : 함수 포인터 (시그널을 처리하는 시그널 핸들러의 포인터 대입)
// 시그널은 쌓이지 않음
// 즉, 동일한 이벤트가 연이어서 다섯 번 발생했다고 해서, 프로세스에게 동일한 시그널을 다섯 번 전달해 주지 않음
// sa_mask에 설정된 시그널들은 동일한 이벤트가 연이어서 다섯 번 발생하는 경우 순차적으로 시그널을 발생시킴
// 즉 첫 번째 시그널이 처리되는 동안 나머지 시그널들은 블로킹 상태에 있게 됨
// sa_mask : 시그널 핸들러 함수가 실행되는 동안에 블로킹될 시그널들을 설정하는 요소
// 주로 모든 비트를 0으로 masking 함
// sa_flags : 시그널을 핸들링하는데 있어서 필요한 옵션을 설정하는 경우에 사용 (기본적으로 0으로 설정)
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
void handler(int sig);
int main(void)
{
int state;
int num = 0;
struct sigaction act;
act.sa_handler = handler;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGINT, &act, 0);
if(state != 0)
{
puts("sigaction() error ");
exit(1);
}
while(1)
{
printf("%d : 대기중 \n", num++);
sleep(2);
if(num > 5) break;
}
return 0;
}
void handler(int sig)
{
printf("전달된 시그널은 %d \n", sig);
}
[실행 결과]
#include <unistd.h>
unsigned int alarm(unsigned int seconds);
// 0 혹은 SIGALRM 시그널이 발생 하기까지 남아 있는 초 단위 시간을 리턴
// seconds : SIGALRM 시그널 발생을 초 단위로 예약함
// 이전에 이미 설정되어 있던 SIGALRM 시그널 예약을 취소하는 목적으로 0 전달도 가능
// 즉, 인자로 0이 전달 되면 예약 되어 있던 시그널은 취소됨
#include <stdio.h>
#include <unistd.h>
#include <signal.h>
#include <sys/types.h>
void timer(int sig);
int main(int argc, char** argv)
{
int state;
struct sigaction act;
act.sa_handler=timer;
sigemptyset(&act.sa_mask);
act.sa_flags = 0;
state = sigaction(SIGALRM, &act, 0);
if(state != 0)
{
puts("sigaction() error ");
exit(1);
}
alarm(5);
while(1)
{
puts("대기중");
sleep(2);
}
return 0;
}
void timer(int sig)
{
puts("예약하신 시간이 되었습니다!! \n");
exit(0);
}
[실행 결과]