파이톨치

[시스템 프로그래밍] Exceptional Control Flow 본문

대학수업

[시스템 프로그래밍] Exceptional Control Flow

파이톨치 2022. 11. 13. 14:54
728x90

프로세서는 한가지 일만 할 수 있다.

프로그램이 여러개 돌아갈 때, 프로세서는 동시에 여러개의 프로그램을 돌리는게 아니라 왔다 갔다 하는 것이다.

왼쪽을 보면서 오른쪽을 볼 수 없으니까, 왼쪽을 봤다가 빠르게 오른쪽을 보는 것과 비슷하다. 

 

우리는 이러한 과정을 Control Flow라고 한다. 

 

Control Flow를 바꾸는 방법에는 2가지가 있다고 한다. Jump and branch / Call and return 이다. 

뭐 4가지 같아 보이지만 착각이다. 

이러한 변화를 주기 위해서 프로그램의 상태를 바꾸어야 한다.

 

때문에 예외적인 제어 흐름들이 있다. Exceptional Control Flow라는 것이 필요하다.

컴퓨터 시스템의 모든 레벨에 존재한다고 한다. 

 

낮은 레벨에서는 Exceptions가 있다. 시스템 이벤트에 반응하여 control flow를 변화시킨다. 

 

높은 레벨에서는 Process context switch OS를 통해서 프로세스의 문맥을 바꾸라는 것이다.

Nonlocal jumps 아니면 로컬이 아닌 점프를 하는 방법도 있다.

아니면 Signals를 보내준다. 

 

우선 Exceptions, 낮은 레벨에서 일어나는 일들을 보자. 

몇몇 이벤트들에 반응해서 OS 커널에 변화를 주는 것이다. 

 

 

다음과 같이 예외가 생기면 커널 코드로 넘어가서 예외를 핸들링하고 중단시킨다. 

 

이러한 예외를 모아둔 테이블을 Exception Tables라고 한다.

얘네 들은 고유한 숫자를 가지고 있다. 발생한 순서대로 번호가 매겨진다.

 

Asynchronous Exceptions 

그럼 동시에 일어나는 예외 상황은? 충돌 상황은?

외부 기기로부터 I/O가 생기면 충돌이 일어난다고 한다. 

키보드로 컨트롤 - c 하면은 생긴다는데 왜지? 잘 이해가 안간다. 

 

다음은 Synchronous[동기·시간·순] 예외이다. 

쓸 수 없는 공간에 무언가 쓴다거나 하는 문제이다. 

Traps, Faults, Aborts 가 있는데 traps은 시스템 콜을 하는 것이고 faults는 내가 잘못한 것이고, aborts는 의도하지 않은 것이다.

 

시스템 콜[system calls]로 read, wirte, open, close, stat, fork, execve, _exit, kill 등의 명령어가 있다. 

Fault Example로는 Page Fault가 있다. 접근하고자 하는 메모리가 아직 없을 때 생기는 에러이다.

invalid 메모리 참조는 a[1000]을 햇는데 a[5000]에 접근하는 경우에 생긴다. 뭐 당연하게 오류이다. 

 

프로세스 : 실행중인 프로그램을 말한다. 2가지 추상화를 가지는데 logical control flow, private address space인데

프로세스는 자기 혼자 있고 자기 혼자 주소를 쓴다고 생각하다는 것이다. 세상에 자기 밖에 없는 착각을 하는 것이다.

이러한 착각을 OS가 처리해준다. 그리고 여기서 들어가는 것이 state 관리이다. 

 

System Call Error Handling [과제 내용]

에러가 났을 때 Linux 시스템 레벨의 함수들은 -1을 반환한다. 그리고 errno라는 글로벌 변수에 저장되는데

strerror(errno)를 하면 에러가 문자열 형식으로 뜬다고 한다.

 

getpid, getppid : 앞에 것은 현재 프로세서의 PID를 반환해주는 것이고 뒤에 것은 부모 프로세스의 PID를 반환해 주는 것이다.

 

프로세스는 running, stopped, terminated 3가지 상태 중 하나이다.

main을 실행하고 리턴하면 끝난다. 아니면 exit을 해도 끝난다. 아니면 종료 신호를 받고 끝날 수도 있다.

이때 exit은 exit(-1)과 같은 형식으로 하는데, -1은 에러를 의미하는 것이다. 0이 아니면 에러!

 

fork?? 프로세스가 생성이 된다는 것은..? shell 프로그래밍? 쉘은 주소를 카피한 다음에 pcb를 따로 만들어서 가리킨다.

명령어도 실행파일 이름? 덮어쓰기 해서 새로운 프로세스를 만들어? 

자식 부모 프로세스? exec으로 덮어쓰기?  

 

예제로 봐야 좀 이해가 간다. 

int main()
{
    pid_t pid;
    int x = 1;
    
    pid=Fork();
    // Child
    if(pid==0) {
    	printf("child : x=%d\n", ++x);
        	exit(0);
    }
    
    // Parent
    printf("parent: x=%d\n", --x);
    exit(0);
}

이렇게 하면 pork를 한 pid는 0이다. 

그래서 자식에서 ++을 해도 부모는 영향을 받지 않는다. 완전히 다른 메모리 공간을 잡고 있기 때문이다. 

 

init 프로세스는 모든 것의 조상인데 pid == 1이라고 한다. 

 

이미 없는데 공감이 남는 경우도 있다. 이를 좀비상태라고 말한다. 예제 코드를 보면 다음과 같다.

 

void fork7() {
 if(fork() == 0) {
  //child
  printf("Terminating child, PID = %d\n", getpid());
  exit(0);
 } else {
  printf("Running Parent, PID = %d\n", getpid());
  while(1); // 무한 루프
 }
}

다음과 돌리고 ps, kill 부모 PID 를 하면 init이 자식을 받아 돌아간다고.. 솔직히 뭔소린지 잘 모르겠다.

 

작업을 기다릴수도 있다. 싱크를 맞추려고 이러한 작업을 한다고 한다. 

 

wait(&child_status);로 사용한다. 

waitpid(pid[i], &child_status, 0);을 하면 자식 프로세스가 모드 끝날때까지 라고 하는 것 같은데...

이것도 뭔소린지...

 

execve : 로딩하고 돌리는 프로그램들이다. 호출은 한번 하는데 반환은 안 해??


# Exceptional Control Flow : Signals 

signals는 kernel software 에서 해주는 것이다.

 

## The World of Multitasking : 멀티 테스킹의 세계

System runs many processes concurrently : 시스템은 많은 프로세스를 동시에 돌린다.

 

Process: executing program, 프로세스는 프로그램 실행하는 것이다.

 

Regularly switches from one process to another : 하나의 프로세스로부터 주시적으로 다른 프로세스로 바뀐다. 

- Suspend process when it needs I/O resource or timer event occurs : I/O 자원이 팔요하거나이나 타이머 이벤트가 발생할 때 프로세스의 중단이 일어난다. 

- Resume process when I/O available or given scheduling priority : 스케줄링 때문에 바뀌기도 한다.

 

Appears to user(s) as if all processes executing simultaneously : 프로세스가 마치 동시에 일어나는 것처럼 유저에게 보여준다.

 

## Programmer’s Model of Multitasking

기본적인 함수로 fork가 있다. 또한 exit, wait, execve와 같은 함수들이 있다. 

execve는 새로운 프로그램을 돌린다. 일반적으로 한번 실행이 되고, 반환되지 않는다.

 

## Shell Programs 

A shell is an application program that runs programs on behalf of the user. :쉘은 어플리케이션 프로그램이다. 

int main() {
	char cmdline[MAXLINE];
    
    while(1) {
    	printf("> ");
        Fgets(cmdline, MAXLINE, stdin);
        if (feof(stdin))
        	exit(0);
        
        eval(cmdline);
    }
}
void eval(char *cmdline) {
	char *argv[MACARGS];
    int bg;
    pid_t pid;
    
    bg = parseline(cmdline, argv);
    if(!builtin_command(argv)) {
    	// 자식으로 갔을 때...
    	if((pid==Fork() == 0) {
        	if(execve(argv[0], argv, environ) < 0) {
            	printf("%s: Command not found.\n", argv[0]);
                exit(0);
            }
        }
        // 부모가 기다린다. fg 가 끝날 때 까지
		if (!bg) {
        	int status;
            if(waitpid(pid, &status, 0) < 0)
            	unix_error("waitfg : waitpid error");
        }
        else 
        	printf("%d %s", pid, cmdline);
	}
}

 

## Background Job 은 무엇인가?

Users generally run one command at a time : 유저는 하나의 커멘드를 한번에 한다.

 

Some programs run “for a long time” : 몇몇은 굉장히 길게 돈다.

- Example: “delete this file in two hours”

 

A “background” job is a process we don't want to wait for : 백그라운드 job은 기다리고 싶지 않은 프로세스이다. 

 

Will become zombies when they terminate

Will never be reaped because shell (typically) will not terminate

Will create a memory leak that could run the kernel out of memory

Modern Unix: once you exceed your process quota, your shell can't run any new commands for you: fork() returns -1

 

## ECF to the Rescue! : ECF 로 구출하자!!

Problem : 쉘이 백그라운드 잡이 언제 끝날지 몰라. 언제든지 일어날 수 있다. 쉘의 정기적인 컨트롤 흐름이 정상적으로 끝날 때에서 제대로 회수를 못할 수도 있다. 

 

Regular control flow is “wait until running job completes, then reap it”

 

Solution: Exceptional control flow, signal이라는 경고문을 보내면 된다. 


# Signals

A signal is a small message that notifies a process that an event of some type has occurred in the system.

- akin to exceptions and interrupts

- sent from the kernel (sometimes at the request of another process) to a process

- signal type is identified by small integer ID’s (1-30)

- only information in a signal is its ID and the fact that it arrived

 

## Sending a Signal

Kernel sends (delivers) a signal to a destination process by updating some state in the context of the destination process : 커널은 시그널을 보낸다.

 

Kernel sends a signal for one of the following reasons 

-  Kernel has detected a system event such as divide-by-zero (SIGFPE) or the termination of a child process (SIGCHLD)

- Another process has invoked the kill system call to explicitly request the kernel to send a signal to the destination process

 

## Receiving a Signal

A destination process receives a signal when it is forced by the kernel to react in some way to the delivery of the signal

 

Three possible ways to react

- Ignore the signal (do nothing)

- Terminate the process (with optional core dump)

- Catch the signal by executing a user-level function called signal handler 

-- Akin to a hardware exception handler being called in response to an asynchronous interrupt

## Signal Handling Example

void int_handler(int sig) {
	safe_printf("Process %d received signal %d \n", getpid(), sig);
    eixt(0);
}

void fork13() {
	pid_t pid[N];
    int i, child_status;
    signal(SIGINT, int_handler);
    for(i=0; i<N; i++) 
    	if((pid[i] = fork() == 0) {
        	while(1);
        }
    for(i=0; i<N; i++) {
    	printf("Killing process %d \n", pid[i]);
        kill(pid[i], SIGINT);
    }
    for(i=0; i<N; i++) {
    	pid_t wpid = wait(&child_status);
        if (WIFEXITED(child_status)) 
        	printf("Child %d terminated with exit status %d \n",
            wpid, WEXITSTATUS(child_status));
        else
        	printf("Child %d terminated abnormally \n", wpid);
    }
}
728x90