[Network] Chapter3 - 유닉스 프로세스

3.1 프로세스의 이해

  • 이 장에서는 정해진 시나리오 없이 자유롭게 메시지 주고받고 싶은 것이 목적이다.

  • talk 프로그램 : server와 client가 1대1로 자유자재로 메시지를 보내는 것.
    • 일반적으로 read, write 함수 등을 쓰면 block 상태가 된다.
    • 그럼 자유로운 전송이 어렵다.
    • 따라서 하나의 클라이언트 프로그램만으로는 부족할 것이다.
    • 그래서 fork() 라는 함수를 사용할 것.
  • chat 프로그램 (4장) : server를 통해서 client끼리 자유롭게 메시지를 보내는 것.


3.1.1 프로세스의 정의

  • 프로세스
    • 실행 가능한 프로그램이 실행중인 상태
      • 메인 메모리에 로드되고 운영체제에 의해 CPU의 서비스를 받을 수 있는 상태
      • 프로그램 : 프로그래밍 언어로 작성된 소스코드로, 일종의 파일. HDD 등 보조 메모리에 저장된 상태
    • 프로세스는 프로세서(CPU)가 수행하는 단위 이다.
  • 프로세스의 모드
    • 사용자 모드 : 사용자 권한으로 명령 실행
    • 커널 모드 : 커널의 권한으로 실행. 시스템에서 HDD를 읽기 위한 read() 함수 등
      • 사용자 모드에서 I/O 명령 수행시, 커널모드로 바뀌어 수행이 되고, 완료하면 다시 사용자로 바뀐다.


프로세스의 상태

int main() {					// 사용자 모드에서 처음 실행 (CPU의 서비스를 받는 실행상태)
	char buf[512];				
	int n = read(0, buf, 512);	// 처리를 위해 커널모드로 전환 (입력을 기다릴동안 블록상태)
	n++;						// 리턴되면 사용자모드로 전환
	exit(0);					// 종료를 위해 커널모드로 전환 (좀비 상태를 거쳐 종료)
}
  • 프로세스가 종료되었다는 의미는, 프로세스에 관한 정보가 메모리에서 모두 사라진 상태를 의미한다.
  • ps 명령으로 프로세스 상태, pid 등의 정보를 알 수 있다.


  • 실행(running) 상태
    • 프로세스가 CPU의 서비스를 받을 수 있는 상태
  • 블록(waiting) 상태
    • I/O 처리나 어떤 조건을 기다리는 등 프로세스에서 스스로 멈추는 상태
  • 중단(stopped) 상태
    • 프로세스 실행이 인터럽트 등 외부의 요청 등에 의해 잠시 멈추는 상태
  • 좀비(zombie) 상태
    • 프로세스 실행은 끝났으나, 부모 프로세스가 종료를 명령하기 전까지 대기하는 상태


3.1.2 프로세스의 메모리 배치

메모리 영역

  • 프로세스 실행을 위해 일정한 (메인)메모리를 배정받아 사용하는데, 이 영역을 프로세스 이미지라고 한다.

  • 원칙적으로 특정 프로세스가 사용하는 메모리영역(이미지)에는 다른 프로세스가 접근할 수 없다.


  • 프로세스 이미지 내용을 알아보기 위한 간단한 C 프로그램 예제
#include <stdio.h>
#include <stdlib.h>
extern char **environ;		// extern 변수
int init_global_var = 3;	// 초기화된 global 변수 (초기값 3)
int unint_global_var;		// 초기화되지 않은 global 변수

int main(int argc, char **argv) {
    int	auto_var;			// 자동 변수
    static int static_var;	// static 변수
    register int reg_var;	// register 변수
    char *auto_ptr;			// 자동 변수
    auto_ptr = malloc(10);	// 메모리를 10byte 할당
    return 0;
}


  • 프로세스의 메모리 영역과 저장되는 변수 종류 예

image-20201102164545138


  • segmentation fault : 내가 접근할 수 없는 영역을 접근하려고 할 때 일어나는 에러..!!


스택과 힙

  • 스택 (Stack)
    • 현재 호출되어 실행중인 함수의 코드와 환경 정보를 저장
    • 함수 내부에서 자동으로 생성되고 임시로 사용되는 자동 변수(지역변수)도 스택 영역에 할당
  • 힙 (Heap)
    • 스택의 경우에는, 함수가 종료되면 사용하던 메모리도 함께 사라진다.
    • 함수가 리턴되어도 사라지지 않도록 한 영역이 힙이다.
    • malloc() 함수, static 등을 사용해 프로그램이 종료될 때까지 존재하는 영역은 힙에 저장된다.


3.1.3 프로세스의 생성과 종료


fork()

  • 새로운 프로세스를 만들기 위해 사용
  • fork()를 호출한 프로세스의 이미지를 복사하여 새로운 프로세스를 생성
  • 부모/자식 프로세스
    • 부모 프로세스 : fork()를 호출한 프로세스
      • fork()의 리턴값 : 자식프로세스의 PID
    • 자식 프로세스 : fork()에 의해 새로 생성된 프로세스
      • fork()의 리턴값 : 0
  • 프로세스의 공유
    • 부모와 자식 프로세스는 변수를 서로 공유하지 않음
    • 개설한 파일이나 소켓은 프로세스 이미지 외부에 존재하므로 공유


fork() 사용 예

  • fork() 를 이용해 부모와 자식프로세스가 각각 다른 일을 수행하는 것을 확인하는 예제 코드
//--------------------------------------------------------
// 파일명 : fork_test.c
// 기  능 : fork() 시스템 콜 사용 예
// 컴파일 : cc -o fork_test fork_test.c
// 사용법 : fork_test
//---------------------------------------------------------

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int global_var = 0;	// 전역 변수 선언
 
int main(void) {
	pid_t pid;
	int local_var = 0;	// 지역 변수 선언

	if((pid = fork()) < 0) {
		printf("fork error\n");
		exit(0);
	} else if (pid == 0) { // 자식 프로세스
		global_var++;
		local_var++;
		printf("CHILD - my pid is %d and parent's pid is %d\n", getpid(), getppid());
	} else {	// 부모 프로세스 pid > 0
		sleep(2);		// 2초 쉰다
		global_var +=5;	// 변수는 공유하지 않는다고 했다!
		local_var +=5;
		printf("PARENT - my pid is %d and child's pid is %d\n", getpid(), pid);
	}

	printf("\t global var : %d\n", global_var);
	printf("\t local var : %d\n", local_var);

	return 0;
}
  • 결과
./fork_test
CHILD - my pid is 6731 and parent's pid is 6730
         global var : 1
         local var : 1
PARENT - my pid is 6730 and child's pid is 6731
         global var : 5
         local var : 5
  • 자식 프로세스는 부모 프로세스의 이미지를 그대로 복사해서 가져간다.
  • 다만 부모와 자식 프로세스는 변수를 공유하지 않음을 알 수 있다.
  • 또한, 프로세스 id를 각자 다르게 가지는 것을 확인할 수 있다.


getpid(), getppid()

pid_t getpid() 	// 내 프로세스의 pid
pid_t getppid() // 부모 프로세스의 pid
  • 자신의 pid, 부모의 pid를 반환하는 함수


프로세스의 종료

  • 종료 조건
    • main() 함수에서 return 되는 경우
    • exit() 함수 호출할 경우
    • 프로세스 종료 signal을 받는 경우
  • 종료 값
    • main()의 return 또는 exit() 호출시 인자
    • 종룟….
    • ㅇㅇ…
  • 즉, 자식 프로세스는 부모 프로세스가 생성과 종료를 담당한다.

###


3.2 토크 프로그램

3.2.1 토크서버 프로그램

  • 서버
    • listen()
    • accept() & fork()
    • 부모 - 입력 및 send(), 자식 - recv() 및 출력
  • 클라이언트
    • connect()
    • fork()
    • send(), recv()


토크 서버측 프로그램

  • tcp_talkserv.c 서버측 코드 예제
//---------------------------------------------
// 파일명 : tcp_talkserv.c
// 기  능 : 토크 클라이언트와 1:1 통신을 한다
// 컴파일 : cc -o tcp_talkserv tcp_talkserv.c
// 사용법 : tcp_talkserv 3008
//---------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>

char *EXIT_STRING = "exit";	// 종료문자
int recv_and_print(int sd);	// 상대로부터 메시지 수신 후 화면 출력
int input_and_send(int sd);	// 키보드 입력 받고 상대에게 메시지 전달

#define MAXLINE 511

int main(int argc, char *argv[]) {
	struct sockaddr_in cliaddr, servaddr;
	int listen_sock, accp_sock,	// 소켓 번호
		addrlen = sizeof(cliaddr);
	pid_t pid;

	if(argc != 2) {
		printf("Usage: %s port_number\n", argv[0]);
		exit(0);
	}

	// 소켓 생성
	if((listen_sock = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
		perror("socket fail");
		exit(1);
	}
	// 서버의 소켓주소 구조체 초기화
	bzero((char*)&servaddr, sizeof(servaddr));

	// servaddr 셋팅
	servaddr.sin_family = AF_INET;
	servaddr.sin_addr.s_addr = htonl(INADDR_ANY);
	servaddr.sin_port = htons(atoi(argv[1]));

	// bind() 호출
	if (bind(listen_sock, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
		perror("bind fail");
	}	
	
	puts("서버가 클라이언트를 기다리고 있습니다.");
	// 소켓 번호를 수동적 소켓으로 listen()
	listen(listen_sock, 5);


	// 클라이언트 연결요청 수락
	if ((accp_sock = accept(listen_sock, (struct sockaddr *)&cliaddr, &addrlen)) < 0) {
		perror("accept fail");
		exit(0);
	}	
	
	puts("클라이언트가 연결되었습니다.");
	
	if ( (pid = fork()) > 0) { // 부모
		input_and_send(accp_sock);	// 키보드 입력받고 상대에게 메시지 전달
	} else if (pid == 0) { // 자식
		recv_and_print(accp_sock);
	}
	close(listen_sock);
	close(accp_sock);
	return 0;
}

// 키보드 입력받고 상대에게 메시지 전달
int input_and_send(int sd) {
	char buf[MAXLINE+1];
	int nbyte;
	while(fgets(buf, sizeof(buf), stdin) != NULL) {
		nbyte = strlen(buf);
		write(sd, buf, nbyte);
		// 종료 문자열 처리
		if (strstr(buf, EXIT_STRING) != NULL) {
			puts("Good bye.");
			close(sd);
			exit(0);
		}
	}
	return 0;
}

// 상대로부터 메시지 수신 후 화면 출력
int recv_and_print(int sd) {
	char buf[MAXLINE+1];
	int nbyte;
	while(1) {
		if ((nbyte = read(sd, buf, MAXLINE)) < 0) {
			perror("read fail");
			close(sd);
			exit(0);
		}
		buf[nbyte] = 0;

		// 종료문자열 수신시 종료
		if (strstr(buf, EXIT_STRING) != NULL) {
			puts("Good bye, server! Client out.");
			kill(getppid(), SIGINT);
                        exit(0);			
		}
		printf("%s", buf); // 화면 출력
	}
	return 0;
}
  • 서버는 클라이언트로부터 connect call 이 오면, 그 시점부터 해당 클라이언트의 정보를 가지고 있는다.
  • 따라서 연결 설정이 된 이후부터는 server나 client 어느 쪽에서든지 먼저 메시지를 보낼 수 있다.


코드 일부 수정

  • 입력 값을 비교하여 exit 을 입력하면 프로그램이 종료되도록 하였는데, 약간의 문제가 있어 수정하였다.
  • 해당 코드의 경우 부모 프로세스에서는 입력을, 자식 프로세스에서는 수신을 담당한다.
  • 이 때, 입력한 문자열이 exit이면 프로그램을 종료한다. 즉, 부모 프로세스를 종료하는 것이다.
  • 마찬가지로 수신한 문자열이 exit이면 프로그램을 종료한다. 하지만 이 때는 자식 프로세스가 종료된다.
  • 자식 프로세스를 종료한다고 해도 부모 프로세스는 존재하기 때문에 계속해서 프로그램이 수행된다.
  • 따라서 수신한 문자열이 exit일 경우 부모 프로세스의 PID를 이용해 kill 함수로 프로세스를 종료해준다.


토크 클라이언트측 프로그램

  • tcp talkcli.c 클라이언트측 코드 예제
//---------------------------------------------
// 파일명 : tcp_talkcli.c
// 기  능 : 토크 서버와 1:1 통신을 한다
// 컴파일 : cc -o tcp_talkcli tcp_talkcli.c
// 사용법 : tcp_talkcli 127.0.0.1 3008
//---------------------------------------------
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <netdb.h>
#include <signal.h>

#define MAXLINE 511

char *EXIT_STRING = "exit";	// 종료문자
int input_and_send(int sd);	// 키보드 입력 받고 상대에게 메시지 전달
int recv_and_print(int sd);	// 상대로부터 메시지 수신 후 화면 출력

int main(int argc, char *argv[]) {
	pid_t pid;
	static int s;
	static struct sockaddr_in servaddr;

	// 명령문 입력 인자 처리
	if(argc != 3) {
		printf("Usage: %s server_ip port_number\n", argv[0]);
		exit(0);
	}

	// 소켓 생성
	if((s = socket(PF_INET, SOCK_STREAM, 0)) < 0) {
		printf("Client: Can't open stream socket. \n");
		exit(1);
	}
	// 서버의 소켓주소 구조체 초기화
	bzero((char*)&servaddr, sizeof(servaddr));

	// servaddr 셋팅
	servaddr.sin_family = AF_INET; // 주소 체계
	inet_pton(AF_INET, argv[1], &servaddr.sin_addr);
    //servaddr.sin_addr.s_addr = inet_addr(argv[1]);
	servaddr.sin_port = htons(atoi(argv[2]));

	// 서버에 연결 요청
	if (connect(s, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
		printf("Client: Can't connect to server. \n");
		exit(0);
	}	

	if ( (pid = fork()) > 0) { // 부모 프로세스
		input_and_send(s);
	} else if (pid == 0) { // 자식 프로세스
		recv_and_print(s);
	}
	close(s);
	return 0;
}

// 키보드 입력받고 상대에게 메시지 전달
int input_and_send(int sd) {
	char buf[MAXLINE+1];
	int nbyte;
	while(fgets(buf, sizeof(buf), stdin) != NULL) {
		nbyte = strlen(buf);
		write(sd, buf, nbyte);

		// 종료 문자열 처리
		if (strstr(buf, EXIT_STRING) != NULL) {
			puts("Good bye.");
			close(sd);
			exit(0);
		}
	}
	return 0;
}

// 상대로부터 메시지 수신 후 화면 출력
int recv_and_print(int sd) {
	char buf[MAXLINE+1];
	int nbyte;
	while(1) {
		if ((nbyte = read(sd, buf, MAXLINE)) < 0) {
			perror("read fail");
			close(sd);
			exit(0);
		}
		buf[nbyte] = 0;

		// 종료문자열 수신시 종료
		if (strstr(buf, EXIT_STRING) != NULL) {
			puts("Good bye, client! Server out.");
			kill(getppid(), SIGINT);
			exit(0);
		}
		printf("%s", buf); // 화면 출력
	}
	return 0;
}
  • 위 서버측 프로그램의 내용을 설명한 부분과 같다. 마찬가지로 코드를 좀 수정하였다.
  • 이제 서버나 클라이언트 어느 쪽에서든지 exit을 하면 양쪽 모두 프로그램이 종료된다.


  • 부모 프로세스가 종료 되면, 자식 프로세스가 종료가 된다.
  • 하지만 자식 프로세스만 종료된다고 부모가 종료되지 않는다.
  • 따라서.. 자식은 끝났지만 부모 프로세스가 아직 안끝났으면 좀비 프로세스 상태가 되는 것이다.







© 2020.07. by Sanggoe

Powered by Sanggoe