Chapter 08. 프로세스
리눅스 프로그래밍 원리와 실제 - 창병모 교수님
8.1 쉘과 프로세스
1) 쉘
- 쉘(Shell)
- 사용자와 운영체제 사이에 창구 역할을 하는 소프트웨어
- 사용자로부터 명령어를 입력받아 이를 처리하는 명령어 처리기(Command Processor) 역할 수행
* 프로세스: 실행 중인 프로그램
- 쉘의 실행 절차
- 쉘은 시작하면 실행 파일을 읽어 실행
- 시작 파일은 환경 변수와 같은 사용자의 사용 환경을 초기화하는데 주로 사용
- bash의 경우 시스템 차원의 시작 파일로 /etc/profile과 /etc/bashrc를 사용하고 사용자 차원의 시작 파일로 ~/.bash_profile과 ~/.bashrc를 사용함
- 쉘은 시작 파일을 실행한 후에 프롬프트를 출력하고 사용자의 명령을 기다림
- 사용자의 명령이 입력되면 이를 해석하여 실행한 후 다시 프롬프트를 출력하는 과정부터 반복
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 명령어를 입력하면 다시 돌아와서 전면에서 실행을 계속함 |
- 후면 처리
- 명령어를 후면에서 처리하고 전면에서는 다른 작업을 할 수 있으면 동시에 여러 작업을 수행할 수 있음
- 다음과 같이 & 기호를 사용하여 후면에서 명령을 실행시킬 수 있음
$ 명령어 &
명령어를 후면에서 실행함
- 시간이 오래 걸리는 작업이나 동시에 여러 작업을 수행하고자 할 때 후면처리를 이용할 수 있음
- 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): 열린 파일 디스크립터, 현재 작업 디렉터리 등과 같은 프로세스의 정보를 저장하는 영역
'Study > Linux' 카테고리의 다른 글
[LinuxProgramming] Chapter 10. 메모리 관리 (0) | 2022.06.07 |
---|---|
[LinuxProgramming] Chapter 09. 프로세스 제어 (0) | 2022.06.06 |
[LinuxProgramming] Chapter 07. 파일 및 레코드 잠금 (0) | 2022.04.25 |
[LinuxProgramming] Chapter 06. 파일 시스템 (0) | 2022.04.25 |
[LinuxProgramming] Chapter 05. 파일 입출력 (0) | 2022.04.25 |