Fascination
article thumbnail

Chapter 08. 프로세스

리눅스 프로그래밍 원리와 실제 - 창병모 교수님


8.1 쉘과 프로세스

1) 쉘

- 쉘(Shell)

  • 사용자와 운영체제 사이에 창구 역할을 하는 소프트웨어
  • 사용자로부터 명령어를 입력받아 이를 처리하는 명령어 처리기(Command Processor) 역할 수행

그림 8.1 쉘의 역할

* 프로세스: 실행 중인 프로그램

- 쉘의 실행 절차

  • 쉘은 시작하면 실행 파일을 읽어 실행
  • 시작 파일은 환경 변수와 같은 사용자의 사용 환경을 초기화하는데 주로 사용
  • bash의 경우 시스템 차원의 시작 파일로 /etc/profile과 /etc/bashrc를 사용하고 사용자 차원의 시작 파일로 ~/.bash_profile과 ~/.bashrc를 사용함
  • 쉘은 시작 파일을 실행한 후에 프롬프트를 출력하고 사용자의 명령을 기다림
  • 사용자의 명령이 입력되면 이를 해석하여 실행한 후 다시 프롬프트를 출력하는 과정부터 반복

그림 8.2 쉘의 실행 절차

 

2) 명렁어 실행

- 명령어 실행

  • 쉘은 사용자가 입력한 명령어를 실행하기 위해 새로운 자식 프로세스를 생성하여 이 프로세스로 하여금 입력된 명령어를 실행하게 함
  • 쉘 내에서 자식 프로세스를 생성 → 이 자식 프로세스가 입력된 명령어를 실행 →  쉘은 자식 프로세스 실행을 끝날 때까지 기다림
$ date
2022. 05. 27. (금) 17:48:58 KST
$ ./hello
hello world!

- 명령어 열(command sequence)

  • 여러 명령어를 순차적으로 실행
  • 쉘은 각 명령어를 위한 자식 프로세스를 순차적으로 생성하여 각 프로세스로 하여금 한 명령어씩 실행하게 됨
$ 명령어1; 명령어2; ...; 명령어n

- 명령어 그룹(command group)

  • 명령어를 순차적으로 실행하는 면에서는 명령어 열과 같음
  • but, 명령어 그룹은 마치 하나의 명령어처럼 사용되어 표준입력, 표준출력, 표준오류를 공유함
$ (명령어1; 명령어2; ...; 명령어n)
  • 입출력 재지정과 파이프를 사용할 때 마치 하나의 명령어처럼 모든 입출력을 재지정 혹은 파이프 처리할 수 있음
$ date; who; pwd > out1.txt
$ (date; who; pwd) > out2.txt

첫번째 명령은 pwd의 결과만 out1.txt에 저장되는 반면에 두번째 명령은 모든 명령어의 결과가 out2.txt에 저장됨

 

3) 전면 처리와 후면 처리

- 전면 처리

  • 명렁어를 입력하면 명령어가 전면에서 실행되며 명령어 실행이 끝날 때까지 쉘이 기다려 줌
  • 전면에서 실행되고 있는 명령어는 필요에 따라 키보드와 모니터로 적당한 입출력을 할 수 있음
  • 명령어를 전면 처리하면 한 순간에 하나의 명령어만 실행할 수 있음
  • 전면에서 실행 중 일 때
명령어 설명
$ 명령어
^C
Ctrl-C를 입력하면 실행 중인 명령어가 강제 종료됨
$ 명령어
^Z
정지됨
Ctrl-Z를 입력하면 명령어 실행이 정지됨
$ fg 정지된 명령어는 fg 명령어를 입력하면 다시 돌아와서 전면에서 실행을 계속함

- 후면 처리

  • 명령어를 후면에서 처리하고 전면에서는 다른 작업을 할 수 있으면 동시에 여러 작업을 수행할 수 있음
  • 다음과 같이 & 기호를 사용하여 후면에서 명령을 실행시킬 수 있음
$ 명령어 &
명령어를 후면에서 실행함

그림 8.3 후면 처리

  • 시간이 오래 걸리는 작업이나 동시에 여러 작업을 수행하고자 할 때 후면처리를 이용할 수 있음
  • fg 명령어를 이용하면 후면 실행되고 있는 작업들 중 하나를 선택하여 전면 실행시킬 수 있음
$ fg %작업번호
작업번호에 해당하는 후면 작업을 전면 작업으로 전환시킴
  • 예시
// 100초 동안 정지한 후에 done 메시지를 출력
$ (sleep 100; echo done)&
[1] 8320

// 현재 디렉터리 밑에서 test.c파일을 찾아 프린트
$ find . -name text.c -print &
[8325]

// jobs 명령어는 후면 실행되고 있는 작업들을 리스트 함
$ jobs
[1]- 실행중		(sleep 100; echo done)
[2]+ 완료			 find . -name test.c -print

// 후면에서 실행 중인 작업 [1]을 전면에서 실행
$ fg %1
(sleep 100; echo done)
  • 후면 프로세스는 모니터에 출력할 수는 있지만 이 경우 전면 프로세스의 출력과 뒤섞일 것
    • 이를 방지하려면 다음과 같이 출력 재지정을 이용하여 후면 프로세스의 출력을 파일에 저장하거나 파이프를 이용하여 메일로 보낼 수 있음
$ find . -name test.c -print > find.txt &
$ find . -name test.c -print | mail euna &
  • 키보드로부터의 모든 입력은 전면 프로세스가 받기 때문에 후면 프로세스는 키보드로부터 입력을 받을 수 없음
    • 후면 프로세스가 실행 중 입력이 필요하면 다음과 같이 입력 재지정을 이용하여 파올로부터 입력을 받을 수 있음
$ wc < inputfile &

 

4) 프로세스 리스트 ps

- 프로그램이 실행되면 이를 프로세스(process) 혹은 작업(job)이라고 함

- ps 명령어

  • 현재 실행 중인 프로세스를 확인
  • 현재 존재하는 프로세스들의 실행 상태를 요약해서 출력
  • 옵션을 사용하지 않으면 자신의 프로세스들만을 출력하며 프로세스 번호, 명령어가 시작된 터미널, 프로세스에 사용된 CPU시간, 그리고 명령어 그 자체를 나열
$ ps
PID		TTY		TIME		CMD
31745	pts/0	00:00:00	bash
59887	pts/0	00:00:00	ps
  • 옵션을 적절히 사용하여 현재 시스템에서 실행되고 있는 프로세스들에 대한 다양한 상태정보를 출력할 수 있음
$ ps [-옵션]
  • 옵션은 BSD 유닉스와 시스템 V의 경우 서로 다른데 리눅스에서는 이 둘을 모두 사용할 수 있음
  • BSD 계열인 경우 일반적으로 ps, ps -a, ps -aux 등을 많이 사용
    • -a: 모든 사용자의 프로세스를 출력
    • -u: 프로세에 대한 더 자세한 정보를 출력
    • -x: 더 이상 제어 터미널을 갖지 않은 프로세스들도 함께 출력
  • 시스템 V 계열인 경우에는 일반적으로 ps, ps -ef 등을 많이 사용
    • -e: 현재 시스템 내에 실행 중인 모든 사용자 프로세스 정보를 출력
    • -f: 프로세스에 대한 좀 더 자세한 정보를 출력
  • 다음과 같은 명령어를 사용하여 현재 시스템 내에서 실행중인 프로세스를 모두 리스트할 수 있음
    • ps -ef | more

 

5) 프로세스 제어

- sleep

  • 지정된 시간만큼 프로세스 실행을 지연시킴
$ sleep 초 // 지연된 시간(초)만큼 프로세스 실행을 지연시킴

- kill

  • 현재 실행중인 프로세스를 강제로 종료시키는데 사용
  • 프로세스 번호 또는 작업 번호를 인수로 명시하여 해당 프로세스를 종료시킴
$ kill 프로세스번호
$ kill %작업번호
// 프로세스 번호(혹은 작업 번호)에 해당하는 프로세스를 강제로 종료시킴

- wait

  • 해당 프로세스 번호를 갖는 자식 프로세스가 종료될 때까지 기다림 → 그 동안 쉘은 중지됨
  • 프로세스 번호를 지정하지 않으면 모든 자식 프로세스가 기다리기를 기다림
$ wait [프로세스번호]
// 해당 프로세스 번호를 갖는 자식 프로세스가 종료될 때까지 기다림. 
프로세스 번호를 지정하지 않으면 모든 자식 프로세스가 끝나길 기다림

- exit

  • exit 명령어는 쉘을 종료하고 종료코드(exit)를 부모 프로세스에게 전달
$ exit [종료값] // 쉘을 종료하고 종료코드(exit code)를 부모 프로세스에게 전달

 

 

8.2 프로그램 실행

1) 프로그램 실행 exec()

- exec()

  • exec() 시스템 호출은 실행될 프로그램의 시작 루틴에서 명령줄 인수와 환경변수를 전달
  • 실행파일에는 C 프로그램의 코드와 더불어 C 시작 루틴(start-up routine)이 포함됨
  • 이 시작 루틴은 exec() 시스템 호출로부터 전달받은 명령줄 인수와 환경 변수 등을 다음과 같이 main()함수를 호출하면서 main()함수에 다시 전달
  • main() 함수에서부터 사용자가 작성한 프로그램의 실행이 시작되고 결국 main()함수의 실해잉 끝나면 main() 함수의 반환 값을 받아 exit함
exit(main(argc, argv));

 

2) 명령줄 인수 argv[]

- 프로그램을 실행할 때 exec() 시스템 호출

  • 실행되는 프로그램에게 명령줄 인수를 전달
  • 실행되는 프로그램의 main() 함수는 argc와 argv 매개변수를 통해서 각각 명령줄 인수의 개수와 명령줄 인수들을 포인터 형태로 전달 받음
int main(int argc, char *argv[]);
argc: 명령줄 인수의 개수
argv[]: 명령줄 인수 리스트를 나타내는 포인터 배열

명령줄 인수 출력은 argv[]와 인덱스를 활용하여 할 수 있음

 

3) 환경 변수

- 환경 변수

  • 쉘이 원래 가지고 있던 것을 쉘이 프로그램을 실행시킬 때 실행되는 프로그램(프로세스)에게 넘겨주게 됨
  • 구체적으로는 전역 변수 environ을 통해 환경 변수와 값의 리스트를 포인터 배열 형태로 전달 받음

- 환경 변수 출력 프로그램

#include<stdio.h>
#include<stdlib.h>

// 모든 환경 변수를 출력함
int main(int argc, char *argv[]){
	char **ptr;
    extern char **environ; // extern 전역 변수 처럼 사용하며 이중 포인터임. 
    					   //즉, 화살표를 두번 따라가야 원하는 내용이 있음
    
    for(ptr = environ; *ptr !=0; ptr++)
    	printf("%s\n", *ptr);
    exit(0);
}

 

4) 환경 변수 값 반환 함수 getenv()

- getenv() 시스템 호출: 특정 환경 변수의 값을 가져올 수 있음

#include<stdlib.h>
char *getenv(const char *name); // 환경 변수 name의 값을 반환. 해당 변수가 없으면 NULL을 반환

- 환경변수 HOME, SHELL, PATH 출력 프로그램

#include<stdio.h>
#include<stdlib.h>

int main(int argc, char *argv[]){
	char *ptr;
    
    ptr = getenv("HOME");
    printf("%s\n",ptr);
    
    ptr = getenv("SHELL");
    printf("%s\n",ptr);
    
    ptr = getenv("PATH");
    printf("%s\n",ptr);
    
    exit(0);
}

 

5) 환경 변수 값 설정

- 환경 변수 설정

  • putenv(): name=value 형태의 스트링을 매개변수로 받아서 환경 변수를 설정
  • setenv(): 환경 변수 name과 값 value를 각각 매개변수로 받아서 환경 변수를 설정
#include<stdlib.h>
int putenv(const char *name);
// name = value 형태의 스트링을 받아서 이를 호나경 변수 리스트에 넣어줌.
// 환경 변수 name이 이미 존재하면 원래 값을 새로운 값으로 대체

int setenv(const char *name, const char *value, int rewrite);
// 환경 변수 name의 값을 value로 설정
// 환경 변수 name이 이미 존재하는 경우에는
// -> rewrite 값이 0이 아니면 원래 값을 새로운 값으로 대체하고 rewrite값이 0이면 그대로 둠

- 환경 변수 삭제

  • unsetenv()
#include<stdlib.h>
int unsetenv(const char *name);
// 환경 변수 name의 값을 지움

 

 

8.3 프로그램 종료

1) 프로그램 종료 방법

- 정상 종료(normal termination)

  • main() 실행을 마치고 리턴하면 C 시작 루틴은 이 리턴값을 가지고 exit() 호출
  • 프로그램내에서 직접 exit()을 호출
  • 프로그램 내에서 직접 _exit()을 호출

- 비정상 종료(abnormal termination)

  • abort(): 프로세스에 SIGABRT 시그널을 보내어 프로세스를 비정상적으로 종료
  • 시그널에 의한 종료

- exit()

  • 모든 열린 스트림을 닫고(fclose), 출력 버퍼의 내용을 디스크에 쓰는(fflush) 등의 뒷정리 후 프로세스를 정상적으로 종료
  • 종료 코드(exit code)를 부모 프로세스에게 전달
#include<stdio.h>
void exit(in status);
// 뒷정리를 한 후 프로세스를 정상적으로 종료시킴
// exit()내에서는 뒷정리 후 _exit()를 호출

- _exit()

#include<stdio.h>
void _exit(int status);
// 뒷정리를 하지 않고 프로세스를 즉시 종료시킴

 

2) 프로그램 시작과 종료 과정

(1) 프로그램은 exec() 시스템 호출에 의해 실행이 시작됨

(2) 프로그램은 실행이 시작되면 C 시작 루틴부터 시작되며 이 루틴에서 main() 함수를 호출함으로써 사용자가 작성한 main() 함수의 실행이 시작됨

(3) main() 함수는 시작되면 필요에 따라 사용자 함수를 호출함. main 함수는 최종적으로 반환함으로써 종료할 수 있으며 이때 반환값이 C시작 루틴에 전달되고 C시작 루틴은 이 값을 가지고 exit()함

(4) 또한 main 함수든 사용자 함수든 어떤 함수에서도 직접 exit() 혹은 _exit() 시스템 호출을 할 수 있음

(5) exit() 시스템 호출을 하면 시스템 내의 _exit() 함수가 실행되고 결국 프로그램은 종료되는데 이 함수는 프로그램을 종료하기 전에 표준적인 I/O 뒷정리를 수행

(6) _exit() 시스템 호출을 하는 경우에는 이러한 두시정리 없이 바로 종료

 

3) exit 처리기

- exit() 시스템 호출에 의해 프로세스가 정상적으로 종료될 때 연린 파일을 닫는 등의 표준적인 I/O 뒷정리는 기본적으로 수행함

- 사용자는 별도의 뒷정리 작업을 하기 위하여 exit()에 의한 프로세스 종료 과정에서 자동으로 수행될 exit 처리기 함수를 등록할 수 있음

- 이를 위해서 사용자는 원하는 뒷정리 작업을 하는 함수를 작성하고 이를 다음과 같은 atexit() 시스템 호출을 이용해 등록해주면 됨

  • 이 처리기 함수는 프로그램이 종료될 때 자동적으로 호출되어 실행됨
  • 32개의 함수를 등록할 수 있음
  • 등록된 exit 처리기는 프로그램이 종료될 때 등록된 역순으로 호출되어 실행됨

- 처리기 예제

#include<stdio.h>
#include<stdlib.h>
static void exit_handler1(void), exit_handler2(void);

int main(void){
	if(atexit(exit_handler1)!=0)
    	perror("exit_handler1 등록할 수 없음");
    if(atexit(exit_handler2)!=0)
    	perror("exit_handler2 등록할 수 없음");
    printf("main 끝\n");
    exit(0);
}

static void exit_handler1(void){
	printf("첫 번째 exit 처리기\n");
}

static void exit_handler2(void){
	printf("두 번째 exit 처리기\n");
}

두 개의 exit 처리기를 등록하고 있으며 종료될 때 등록된 역순으로 호출되어 실행됨

 

 

8.4 프로세스 ID와 프로세스의 사용자 ID

1) 프로세스 ID

- 프로세스: 실행 중인 프로그램

  • 한 프로그램은 여러번 실행될 수 있으므로 한 프로그램으로부터 여러 개의 프로세스를 만들 수 있음
  • 프로그램 그 자체가 프로세스는 아님

- 각 프로세스는 프로세스를 구별하는 번호인 프로세스 ID를 가짐

  • 각 프로세스는 자신을 생성해준 부모 프로세스가 있음
#include<unistd.h>
int getpid(); // 프로세스의 ID를 리턴
int getppid(); // 부모 프로세스의 ID를 리턴
  • 프로세스 번호 출력
#include<stdio.h>
#include<unistd.h>

// 프로세스 번호를 출력
int main(){
	printf("나의 프로세스 번호 : [%d]\n", getpid());
    printf("내 부모 프로세스 번호: [%d]\n", getppid());
}

부모 프로세스: 쉘 프롬프트를 내주고 있는 쉘 프로세스

  • 각 프로세스는 현재 작업 디렉터리를 가지고 있음
    • chdir() 시스템 호출: 현재 작업 디렉토리를 매개변수가 지정한 경로 pathname으로 변경
    • 이 시스템 호출이 성공하려면 프로세스는 그 디렉터리에 대한 실행 권한이 있어야 함
#include<unistd.h>
int chdir (char *pathname);
// 현재 작업 디렉토리를 pathname으로 변경함
// 성공하면 0 실패하면 -1을 리턴

 

2) 프로세스의 사용자 ID와 그룹 ID

- 프로세스는 프로세스 ID 외에 그 프로세스를 실행시킨 사용자 ID와 그 사용자의 그룹 ID를 가짐

- 프로세스의 사용자 ID와 프로세스의 그룹 ID는 프로세스가 수행할 수 있는 권한을 검사하는데 사용

- 프로세스의 실제 사용자 ID(real user ID)

  • 그 프로세스를 실행한 원래 사용자의 사용자 ID로 설정됨
  • 예를 들어 chang이라는 사용자 ID로 로그인하여 어떤 프로그램을 실행시키면 그 프로세스의 실제 사용자 ID는 chang이 됨

- 프로세스의 유효 사용자 ID(effective user ID)

  • 현재 유효한 사용자 ID로 새로 파일을 만들 때나 파일에 대한 접근 권한을 검사할 때 주로 사용됨
  • 보통 유효 사용자 ID와 실제 사용자 ID는 특별한 실행파일을 실행할 때를 제외하고는 동일함

 

- 프로세스의 사용자 ID와 그룹 ID의 시스템 호출

#include<sys.types.h>
#include<unistd.h>
uid_t getuid(); // 프로세스의 실제 사용자 ID를 반환
uid_t geteuid(); // 프로세스의 유효 사용자 ID를 반환
uid_t getgid(); // 프로세스의 실제 그룹 ID를 반환
uid_t getegid(); // 프로세스의 유효 그룹 ID를 반환

int setuid(uid_t uid); // 프로세스의 실제 사용자 ID를 uid로 변경
int seteuid(uid_t uid); // 프로세스의 유효 사용자 ID를 uid로 변경
int setgid(gid_t gid); // 프로세스의 실제 그룹 ID를 gid로 변경
int setegid(gid_t gid); // 프로세스의 유효 그룹 ID를 gid로 변경

 

3) 프로세스의 사용자 ID 예제

- 프로세스의 실제/유효 사용자 ID와 실제/유효 그룹 ID를 조사해보는 프로그램

#include<stdio.h>
#include<pwd.h>
#include<grp.h>
#include<unistd.h>

int main(){
	printf("나의 실제 사용자 ID: %d(%s)\n",getuid(), getpwuid(getuid())->pw_name);
    printf("나의 유효 사용자 ID: %d(%s)\n",geteuid(), getpwuid(geteuid())->pw_name);
    printf("나의 실제 그룹 ID: %d(%s)\n",getgid(), getgrgid(getgid())->gr_name);
    printf("나의 유효 그룹 ID: %d(%s)\n",getegid(), getgrgid(getegid())->gr_name);
}

사용자 이름이나 그룹 이름으로 출력하기 위해서는

getpwuid() 함수와 getgrgid() 함수를 사용하여 이름으로 변환해야 함

 

4) set-user-id 실행권한

- 특별한 실행권한 set-user-id(set user ID upon execution)

  • set-user-id가 설정된 실행파일을 실행하면, 이 프로세스의 유효 사용자 ID는 그 실행파일의 소유자로 바뀌게 되어 결과적으로 이 프로세스는 실행되는 동안 그 파일의 소유자 권한을 갖게됨

- set-user-id 실행권한이 설정된 실행파일의 예

  • 패스워드를 변경하는 /bin/passwd 명령어
  • 소유자의 실행 권한이 x가 아니고 s로 표시된 것을 볼 수 있음

(1) 이 실행파일은 set-user-id 실행권한이 설정된 파일이며 소유자는 root임

(2) 일반 사용자 euna가 다음과 같이 이 파일을 실행하게 되면 실행 파일의 소유자인 root가 이 프로세스의 유효 사용자 id가 됨

$ /bin/passwd

(3) 이제 이 프로세스는 유효 사용자 ID가 root이므로 root만 수정할 수 있는 암호 파일 /etc/shadow 파일을 접근하여 수정할 수 있음

 

5) set-user-id 실행권한 설정

- set-user-id 실행권한 설정은 심볼릭 모드로 's'로 표현되고 8진수로는 4000으로 표현

  • set-user-id 실행권한 설정
$chmod 4755 file1

- set-group-id 실행권한이 포함되어 있는 실행 파일은 실행되는 동안에 그 파일의 소유자의 그룹을 프로세스의 유효 그룹 ID로 갖게되며 8진수로는 2000으로 표현됨

  • set-group-id 실행권한 설정
$ chmod 2755 file1

- 실행 예시

uid 프로세스의 유효 사용자 ID가 이 파일의 소유자인 root가 되어 있는 것을 확인할 수 있음

 

 

8.5 프로세스 이미지

1) 프로세스

- 프로세스는 실행중인 프로그램

- 프로그램 실행을 위해서는 프로그램의 코드, 데이터, 스택, 힙, U-영역 등이 필요

- 프로세스 이미지(구조) = 메모리 내의 프로세스 레이아웃

- 프로그램 자체가 프로세스는 아님

 

2) 프로세스 구조

- 텍스트(text): 프로세스가 실행하는 실행코드를 저장하는 영역

- 데이터(data): 전역 변수(global variable) 및 정적 변수(static variable)를 위한 메모리 영역

- 힙(heap): 동적 메모리 할당을 위한 영역. C언어의 malloc 함수를 호출하면 이 영역에서 동적으로 메모리를 할당해줌

- 스택(stack area): 함수 호출을 구현하기 위한 실행시간 스택(runtime stack)을 위한 영역으로 활성 레코드(activation record)가 저장됨

- U-영역(user-area): 열린 파일 디스크립터, 현재 작업 디렉터리 등과 같은 프로세스의 정보를 저장하는 영역

profile

Fascination

@euna-319

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!