수업자료ppt 5장의 9번째 ppt부터 시작하는 내용이다.
커널이 한번 스타트가 되버리면 이제 더이상 부트로더는 의미가 없다.
커널이 스타트되면서 부트로더가 있던 영역을 덮어쓰면서 부트로더는 임무를 마치고 사라지게 된다.
애초에 부트로더의 역할은 부팅을 정상적으로 해서 커널을 작동시키는 것이었다.
IPC
: 여러개의 프로세스 간의 통신이 필요하다. 그걸 위해 구현해놓게 IPC이다. 가장 기본적인것은 시그널이고, 소켓이나, 파이프, message queue, shared memory로도 IPC가 구현된다.가상 파일 시스템(Virtual filesystem)
: 메모리가 부족하면, 파일의 일부분을 마치 메모리처럼 사용한다.(swap memory) // 또는 디바이스를 모두 파일이라고 간주하면서 다루는 것도 가상파일시스템이라고 한다.Process scheduler
: 우분투는 250hz이고, 라즈베리파이는 100hz이다. 헤르츠에 따라서 프로세스 스케줄러는 컨텍스트 스위칭을 하게 된다. (자세한 내용은 아래를 참고) 즉 여러 프로세스가 CPU를 공평하게 사용할 수 있도록 해주는 것이다.Network Interface
: 표준 네트워크 프로토콜과 드라이버를 제공한다.메모리 관리(memory manager)
: 여러 개의 프로세스 메인 메모리를 안전하게 공유할 수 있도록 한다.여기서 말하는 hz는 kernel timer에 대한 hz를 말하는 것이다.
(CPU Clock hz랑 다른 것이다.)
우분투는 250hz로 4ms만 주고, 라즈베리파이는 100hz로 10ms만 준 이유는 두 머신의 컴퓨팅 파워가 다르기 때문이다.(우분투는 4ms만 줘도 엄청나게 많은 작업이 가능하고, 라즈베리파이는 우분투에 비해 컴퓨팅 파워가 좀 낮기 때문에 10ms는 줘야 우분투랑 좀 비슷한 수준의 작업량을 수행할 수 있다는 것이다.)
커널 타이머 헤르츠로 250hz를 쓰겠다는 것은
f = 250hz라는 것이다. (250번 헤르츠가 진동하면서 기계어가 실행되는 동안 실행을 보장해주겠다는 것이다.)
T = 1/250 = 4ms의 시간동안 실행을 보장해주겠다는 것이다.
그 이후에는 강제적으로 문맥을 전환시켜주겠다는 것이다.
ubuntu@ubuntu14:~/pi_bsp/kernel/linux$ vi .config
T = 1/100 = 10ms이므로 라즈베리파이는 하나의 프로세스에 대해 10ms 시간동안 실행을 보장해주겠다는 것이 된다.
4ms, 10ms가 타임 슬라이스 되어 프로세스에게 주어지는 한번의 단위 수행시간이라고 할 수 있겠다.
RTOS에서는 Task라는 용어를 많이 사용하고, 리눅스에서는 Process라는 용어를 많이 사용한다.
전역변수인데 초기화되지 않은 변수들은 BSS 영역에 잡히게 된다.
코어가 여러개여야 "진짜"로 동시에 기계어가 실행될 수 있는 것이지, 멀티 프로세싱이라고 해서 진짜로 동시에 기계어가 실행되는 것은 아니다(컨텍스트 스위칭이 일어나면서 동시에 실행되는 것처럼 느껴질 뿐이다).
스케줄러는 여러 프로세스가 CPU를 공유하여 사용 가능하도록 해준다.
리눅스는 선점형과 비선점형이 좀 혼재되어있는 형태이다.
그래서 보통 RTOS라고 하면 우선순위에 굉장히 민감해서 우선순위가 높은게 들어오면 그 어느순간에도 선점이 가능해진다.
CPU의 사용권을 다른 프로세스에게 양도한다. (현재의 Context 정보를 저장하고 새 프로세스의 Context 정보를 적재) --> 이러한 정보는 "task_struct" 구조체를 통해 관리된다.
좀비프로세스는 프로세스가 분명 종료 되었는데, 컨텍스트 정보가 남아있는 상태를 말한다. (이런 일이 발생하지 않으려면, 반드시 부모프로세스가 자식프로세스를 생성했다면 wait()함수를 통해서 자식프로세스의 자원을 회수해야 한다.)
생성(created) 상태
: 커널 공간에 PCB등이 만들어지고 프로세스가 처음 생성되는 상태이다.준비(ready) 상태
: 기억 장치 등 필요한 모든 자원을 할당 받은 상태에서 프로세서를 할당 받으려고 대기하는 상태이다. 즉, 프로세서를 할당 받게되면 즉시 실행이 가능한 상태이다.실행(running) 상태
: 프로그램 코드가 프로세서에 의해 실행되고 있는 상태, 프로세스가 필요한 모든 자원을 할당 받은 상태이다.대기(blocked) 상태
: 프로세스가 필요한 자원을 요청하고 이를 할당 받을 때까지 기다리는 상태이다. (ex. 실행하다 I/O 등에 의해서 중단된 상태)지연 (suspended) 준비 상태
: 프로세스가 기억장치를 제외한 다른 모든 필요한 자원들을 보유한 상태이다.지연 (suspended) 대기 상태
: 프로세스가 대기 상태에서 기억 장치를 잃은 상태이다.종료(exit) 상태
: 프로세스의 실행을 종료하였으나 아직 프로세스에 대한 정보가 남아있는 상태이다. Zombie 상태라고도 한다.디스패치(dispatch) 또는 스케줄(schedule)
:준비 상태에서 프로세서를 할당 받아 실행 상태로 전이하게 되는 이벤트이다.선점(preemption) 또는 time runout
: 실행 상태의 프로세스가 프로세서 시간 할당량 끝나거나 우선순위가 높은 프로세스가 들어왔을 때 프로세서를 반납하고 준비상태로 전이하게 되는 이벤트이다.대기(block)
: 실행 상태의 프로세스가 자원을 요청하여 대기 상태로 전이하게 되는 이벤트이다.웨이크업(wakeup)
: 대기(지연대기) 상태에서 프로세스가 요청한 자원이 할당되어 준비(지연준비) 상태로 전이하게 되는 이벤트이다.스왑 인(swap-in) 또는 재활동(resume)
: 지연준비(지연대기) 상태에서 기억장치를 할당받아 준비(대기) 상태로 전이하게 되는 이벤트이다.스왑 아웃(swap-out) 또는 지연(suspend)
: 지연 (대기) 상태에서 기억장치를 잃어 지연 준비(지연 대기) 상태로 전이하게 되는 이벤트이다.
- 경량급 프로세스 생성을 위한 clone() 시스템 콜
- 전통적인 프로세스 생성을 위한 fork() 시스템 콜
- 빠른 프로세스 생성을 위한 vfork() 시스템 콜
위 세개의 시스템 콜은 서로 다른 인자를 담아서 다시 한번 do_fork()라는 시스템 콜을 호출한다.
do_fork() 함수는 자식 프로세스를 생성하고 PID를 반환한다.
vfork()가 fork()와 가장 다른 점은 "부모프로세스의 text, data 세그먼트를 복사하지 않는다는 것"이다. 그렇기 때문에 빠른 프로세스 생성이 가능한 것이다.
호출된 새로운 프로그램으로 프로세스를 교체하여 실행
- execl()
- execlp()
- execle()
- execv()
- execvp()
- execvpe()
위 함수들에 대한 자세한 설명은 교재 277페이지를 참고하라
fork()는 부모프로세스와 완전히 동일한 자식프로세스를 만들어낸다.
아래 코드는 자식 프로세스를 만들어서 "ls" 명령어를 실행하는 코드이다.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
int fatal(char *s);
int main(void)
{
pid_t pid;
int status;
switch (pid = fork())
{
case -1:
fatal("fork failed.");
break;
case 0: /* 자식 프로세스 */
sleep(60);
execl("/bin/ls", "-l", "./test", NULL);
fatal("exec failed.");
break;
default: /* 부모 프로세스 */
//wait((int *)0); /* 부모는 자식이 종료될 때까지 대기 */
wait(&status); /* 부모는 자식이 종료될 때까지 대기 */
printf("ls completed %d\n", status >> 8);
exit(0);
}
}
int fatal(char *s)
{
perror(s);
exit(1);
}
<이해하기 포인트>
fork가 성공하면 child 프로세스를 생성 → parent 프로세스에게는 child 프로세스의 PID 반환 / child 프로세스에게는 0 반환
그런데 child 프로세스도 같은 copy된 main프로세스를 실행하게 되니 pid_t pid = fork(); 가 또 실행되고 이게 무한히 반복되는 것은 아닌가? 하는 의문이 들었다.
하지만 위에서 적은 것처럼 부모의 PCB도 fork()를 통해 자식에게 복사된다(PPID와 PID만 다르다).
그리고 PCB에는 CPU에서 수행되던 레지스터의 값들이 저장되어 있고, 여기에는 Program Counter의 정보도 존재한다.따라서 child 프로세스에서도 fork 이후부터 코드가 실행된다.
[참고: fork에 대해 잘 정리된 블로그]
결과적으로 복제 후 실행(Fork() -> execl())이 가장 오버헤드가 적게 자식 프로세스를 생성하는 방법이다. 다시 말해, 그냥 새로 메모리 공간을 할당하여 새로운 프로그램을 로드하면서 자식프로세스를 생성하는 것보다 부모 프로세스를 복제하고 COW(Copy On Write) 하는 것이 마치 이미 만들어져 있는 탬플릿을 가져다 쓰는 느낌으로 동작하게 되어 훨씬 적은 오버헤드로 프로세스를 생성할 수 있다는 것이다.
모든 프로세스의 조상은 1번 프로세스인 systemd 이다.
$ ps -ef | more
위 명령을 통해 많은 커널 스레드를 확인할 수 있다.
$ cd /etc/rc5.d
$ ls -al
위 명령을 통해 거기에 있는 파일들을 보면 리눅스가 부팅되면서 커널이 뜨고 init 프로세스가 생성되는데 필요한 파일 들이 담겨있다.
각각의 프로세스를 유일하도록 구분하는 숫자 값
각각의 파일을 유일하도록 구분하는 숫자 값
cf)
만약 segmentation fault가 나서 exception이 난다면, "???" 명령을 통해서 core라는 파일을 만들도록 하고, GDB라는 프로그램을 통해서 core파일을 보면서 어디서 메모리 접근 오류가 나서 segmentation fault가 났는지 알 수 있다.
컴파일 할 때는 얼마나 쓸지 알 수 도 없는 메모리에 대해서
객체지향에서 객체는 힙영역에 메모리가 잡히기 때문에 ㄱㅊ은데, 하지만 기본적으로 배열은 정적할당이 국룰이다.
다 사용했다면 free()함수를 통해서 반드시 반환해야 한다.
커널에 공유 메모리 공간을 요청하고, 프로세스마다 그 공유 메모리에 접근하는 것이다.
파일의 일부나 장치의 메모리를 프로셋의 주소 공간의 일부로 매핑하여 사용
사용자 영역과 커널 영역은 분명히 다른 메모리 영역에 있기 때문에, 메모리를 복사하여 가져다 써야한다. 그래서 예를 들어, 카메라의 영상을 커널에서 읽어와서 사용자 영역에 뿌릴 때 그 사이사이에 전부 복사 명령을 넣어주면 너무 오버헤드가 커진다. 그래서 바로 이러한 경우에, 사용자 영역에서 커널 영역의 메모리를 직접 접근하여 사용하게 되는데 그 때 사용하는 system call이 mmap()이다.
라즈베리파이는 기본 스왑메모리가 되게 작게 잡혀있다. 그 스왑메모리를 확장해주면 나중에 OpenCV나 ROS를 빌드 할 때 한결 수월하게 할 수 있다.
프로세스가 시작하면서 모든 페이지 프레임을 할당하여 사용하지 않고, 일부 페이지 프레임만 할당하여 사용하다가, 새로운 프레임 요구가 있을 때 새로운 페이지 프레임을 할당하여 사용하는 것..
이로 인해 똑같은 메모리의 양으로 더 많은 작업을 처리할 수 있다.
프로세스가 처음부터 자신의 메모리 공간에 있는 모든 영역을 접근하지 않는다.
프로그램은 실행되는 각 단계에서 보통 프로세스 페이지의 일부만을 사용한다.
디스크의 일부 공간을 램의 확장으로 사용하는 것
커널 쓰레드 kswapd
스왑 아웃되는 페이지
jiffies라는 변수를 이용하여 시스템이 시작하고 나서부터 지나간 틱수를 기록한다.
커널 초기화 때 0으로 설정되고 타이머 인터럽트가 발생할 때마다 1씩 증가한다.