프로세스간의 접근권한이 낮은 경우, 예를 들어 A프로세스에서 B프로세스의 데이터/코드를 바꿀 수 있다면 이는 보안 상 매우 위험하므로, 이러한 커뮤니케이션 방식은 제공되지 않는다. 다만 프로세스 간 커뮤니테이션이 필요한 경우 제공되는 기법이 IPC이다.
프로세스 간 통신은 성능을 높이기 위해 동시에 여러 프로세스를 만들어 실행하는 경우 프로세스 상태 확인, 결과를 통해 만들어진 데이터 송수신을 위해 필요할 수 있다.
동시 작업을 위해 하나의 프로세스를 여러 개의 프로세스로 쪼개서 사용하는 fork의 경우
sys_fork()를 통해 프로세스 자신을 복사하여 자식 프로세스 생성
동시 작업을 위해 여러 프르세스를 fork하여 동시 수행과 같은 병렬 처리를 수행할 것이고 이를 위해 프로세스 상태 확인 및 여기서 발생하는 데이터 처리를 위해 IPC가 필요할 것이다.
이러한 IPC 작업을 이해서는 커널 공간을 사용한다.
프로세스가 메모리에 올라갈 때는 일련의 변환 과정을 거쳐 가상 메모리를 사용하게 되고,크게 Kernel Space와 User Space로 나뉘는데 실제 물리 메모리에는 Kernel Space가 올라가고 동일한 부모 프로세스를 가진 자식 프로세스들에게는 Kernel space가 공유된다.
IPC기법
공유 가능한 저장매체(파일)를 활용(커널 공간 사용 X)
pipe 기법
unistd.h 헤더 파일에 존재하는 pipe()함수를 통해서 사용할 수 있으며, 반이중 방식(두 개의 스트림이 각각 양방향으로 통신을 할 수 있으나, 하나의 통신을 할 때 한 방향으로만 진행이 가능)으로 통신을 하고, 부모 프로세스-자식 프로세스 관계의 프로세스끼리 통신이 가능하다.
함수의 원형은
int pipe(int pipefd[2]);
로, 인자로 크이가 2인 정수형 배열이 들어간다.
파이프는 커널 영역에 생성되고, 파이프를 생성한 프로세스는 fd만을 가지고 있게 된다. 여기서 fd[1]은 write-only, fd[2]는 read-only권한이다. 즉, 우리가 fd[1]으로 데이터를 쓴다면 fd[0]으로 그 데이터를 읽어들일 수 있다.
한 쌍의 부모 프로세스와 자식 프로세스의 통신을 위해서는 2개의 파이프가 필요하다. 파이프를 1개만 사용한다면 통상적인 상황에서는 문제가 없으나, 부모 프로세스가 파이프에 데이터를 쓰자마자 이를 읽으면 파이프에 있는 데이터는 바로 없어질 것이다. 이 때, 자식 프로세스는 파이프의 데이터를 읽기 위해 계속 listen을 하고 있으므로 프로그램이 동작하지 않을 것이다.
이러한 이유로 파이프를 2개 사용한다. fd_A[2] array와 fd_B[2]array를 사용한다고 가정하고
이렇게 파이프를 만들면 부모 프로세스는 자식에게로부터 쓰여지는 fd_B[1]과 자식의 데이터를 읽는 fd_A[0]은 필요 없으니 이를 닫아 주면 되고 자식 프로세스는 반대로 fd_B[0]과 fd_A[1]를 닫아 주면 됩니다.
프로세스 간 쓰레드와 큐를 활용하여 정보를 교환하는 IPC 기법 중 하나
pthread_create(&msgqueue_rx_thread, NULL, Msgqueue_RX_Thread, NULL) < 0;
pthread 함수를 통해 메시지를 수신할 스레드 생성 (Msqqueue_RX_Thread 함수 수행)
struct Msg_type_1
{
long type;
uint32_t data;
};
static void *Msgqueue_RX_Thread(void *arg)
{
int result;
struct Msg_type_1 msg;
while(1)
{
result = msgrcv(MSG_QUEUE_ID, &msg, sizeof(msg) - sizeof(long),1, 0);
}
return NULL;
}
수신 프로세스 : msgrcv() 함수를 통해 지정된 타입에 맞는 메세지 수신 및 수신된 메세지 처리
msgqueue_id = msgget((key_t)MSG_QUEUE_ID, IPC_CREAT | 0666);
struct Mag_type_1 = msg;
while(1)
{
memset(&msg, 0, sizeof(msg));
msg.type = 1;
msg.data = 1;
if(msgsnd(msgqueue_id, &msg, sizeof(msg) - sizeof(long), IPC_NOWAIT) < 0)
{
perror("msgsnd()");
}
}
송신 프로세스: 메시지큐를 열고(msgget()) 메시지큐에 메시지를 전송한다.(msgsnd())
커널 공간에 메시지 큐가 생성되어 작동하며, 메시지 큐를 ID를 맞춰줄 수 있다면 상호간 어떠한 프로세스이더라도 통신이 가능하고, 큐 형태이므로 FIFO 순서로 데이터가 처리된다.
Kernel Space에 메모리 공간을 만들고 해당 공간을 변수처럼 사용하는 방식으로, 해당 메모리에 대해서 변수를 활용하듯 접근을 할 수 있고 공유메모리 Key를 가지고 있다면 여러 프로세스가 접근하여 사용할 수 있다.
공유 메모리를 처음 생성할 때만 System Call을 하기 때문에 속도가 빠르다는 장점이 있으나, 동기화에 대한 관리가 필요하다.
공유메모리 관련 함수
int shmget(key_t key, size_t size, int shmflg);
void *shmat(int shmid, const void* shmaddr, int shmflg);
int shmdt(const void* shmaddr);
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
커널 및 프로세스에서 다른 프로세스에 대한 이벤트 발생을 알려주는 방법으로, 프로세스는 시그널 관련 코드에 핸들러를 등록하여 시그널에 대한 처리를 진행한다. 시스널에 대한 처리는 커널 모드에서 유저 모드로 돌아갈 때 처리되며, 처리 방식은 다음과 같이 나뉠 수 있다.
시그널은 PCB 안에 있는 시그널 관련 정보를 관리하는 구조체에 의해 관리된다. 이전 글인 interrupt에서 다륐던 task_struct라는 구조체 내부에 다음과 같은 구조체가 있다.
struct signal_struct *signal;
struct sighand_struct *sighand;
이름 | 설명 | 기본처리 |
---|---|---|
SIGHUP(HUP) | HangUP의 약어로 터미널에서 접속이 끊겼을 때 보내지는 시그널. 데몬 관련 환경 설정 파일을 변경시키고 변화된 내용을 적용하기 위해 재시작할 때 이 시그널이 사용된다. | 종료 |
SIGINT(INT) | 키보드로부터 오는 인터럽트 시그널로 실행을 중지. [CTRL] + [C] 입력 시에 보내지는 시그널. | 종료 |
SIGQUIT(QUIT) | 키보드로부터 오는 실행 중지 시그널. [CTRL] + [달러] 입력 시에 보내지는 시그널. 기본적으로 프로세스를 종료시킨 뒤 코어를 덤프. | 코어 덤프 |
SIGILL(ILL) | illegal instruction의 약자. 잘못된 명령을 사용했을 때 발생. | 코어 덤프 |
SIGTRAP(TRAP) | trace(추적),breakpoint(중지점)에서 TRAP 발생할 때 | 코어 덤프 |
SIGABRT | abort의 약자로 비정상종료 함수에 의해 발생.(즉 abort 시스템 호출을 하였을 때 발생) | 코어 덤프 |
SIGBUS | 메모리 접근 에러시 발생하는 시그널. | 코어 덤프 |
SIGKILL(KILL) | KILL! 무조건 종료, 즉 프로세스를 강제로 종료시키는 시그널 | 종료 |
SIGCHLD(child) | 자식 프로세스가 stop 되거나 종료되었을 때 부모에게 전달되는 신호. | 무시 |
SIGSCONT(CONT) | Continue의 약자로 STOP 시그널에 의해 정지된 프로세스를 다시 실행시킬 때 사용. | 재시작 |
SIGSTOP(STOP) | 터미널에서 입력된 정지 시그널. SIGCONT로 재실행시킬 수 있다. | 중지 |
SIGSTP(TSTP) | 실행 정지 후 다시 실행을 계속하기 위해 대기시키는 시그널이다. [ctrl] + [z]를 입력했을 때 보내지는 시그널이다. SIGCONT로 역시 다시 실행시킬 수 있다. | 중지 |
SIGIO | 비동기 입출력이 발생했을 경우(I/O now possible!) | 종료 |
이 외에도 다양한 시그널이 있고, 터미널에서 kill -l을 입력하면 확인할 수 있다. 시그널을 핸들링 하기 위해서는 signal 함수를 이용한다.
static void sig_handler(int signo)
{
...
exit(EXIT_SUCCESS);
}
int main(void)
{
if(signal(SIGINT, signal_handler) == SIG_EPR)
{
printf("can't catch sigint\n");
exit(EXIT_FAILURE);
}
...
return 0;
}
멀티태스킹 운영 체제에서 데몬은 사용자가 직접적으로 제어하지 않고, 백그라운드에서 돌면서 여러 작업을 하는 프로그램을 말한다.
코어덤프(core dump)는 어떤 프로그램의 메모리 사용 상태를 특정 시점에서 기록한 것으로, 주로 파일로 만들어진다.
abort 함수는 프로그램 자체에 아주 치명적인 오류가 발생해서 어쩔수 없이 프로그램을 종료해야만 하는 경우에 호출하도록 정의된 함수이다.
프로그램이 네트워크를 통해서 데이터를 통신할 수 있도록 하는 연결부로, 통신할 두 프로그램(Client-Server)에 모두 소켓이 생성되어야 한다.
socket() 생성
-> 다른 노드들과 통신하기 위한 소켓 생성
bind()
-> 자신이 이용하고자 하는 ip와 port를 socket에 할당
listen()
-> 다른 노드에서의 요청 받아들이기 위한 대기상태로 만들어주는 함수
accept()
-> 다른 노드(클라이언트)에서 연결 요청이 왔을 때 이에 대한 응답(수락)
read()/write()
-> 다른 노드와의 연결 이후 데이터 송/수신
close()
-> 다른 노드와의 상호작용이 끝나면 소켓을 닫고 종료
int socket(int domain, int type, int protocol);
socket() 함수
domain: 어떤 영역에서 통신할 것인지를 지정(ex : AF_UNIX, AF_INET, AF_INET6 등)
type : 어떤 서비스 타입의 소켓을 생성할 것인지(ex: SOCK_STREAM(TCP), SOCK_DGRAM(UDP) 등)
protocol: 소켓에서 사용할 프로토콜이 어떤 것인지(ex: IPPROTO_TCP, IPPROTO_UDP, 0(type에서 지정) 등)
리턴값은 정상 생성 시 0 이상의 값(fd)을 , 생성 실패 시 -1을 반환
int bind(int sockfd, strcut sockaddr *myaddr, soklen_t addrlen);
bind() 함수
int listen(int sockfd, int backlog);
int accept(int sockfd, struct sockaddr *addr, socklen_t *addrlen);
accpet() 함수
Unix OS에서 네트워크 소켓과 같은 파일이나 기타 입력/출력 리소스에 액세스하는 데 사용되는 추상표현이다