✅ 백준 1문제 풀기
✅ CS:APP | 7장, 8.1 ~ 8.2
☑️ CS:APP | 8장, 9장
☑️ c++ 공부
링커는 실행 파일 만들 때 쓰는 것 뿐만 아니라 런타임 때도 쓰여서 실제 메모리 주소를 할당하는 건가?
링커는 두 가지 주요 상황에서 사용됩니다: 컴파일 타임(정적 링크)과 런타임(동적 링크)입니다. 두 상황 모두에서 링커는 심볼 해석과 메모리 주소 할당을 수행하지만, 그 방법과 시기는 다릅니다.
정적 링크는 컴파일 타임에 이루어지며, 이 과정에서 링커는 여러 개의 재배치 가능한 객체 파일을 하나의 실행 가능한 목적 파일로 결합합니다. 이 단계에서 링커는 다음 작업을 수행합니다:
1. 심볼 해석: 각 심볼 참조를 정확히 하나의 심볼 정의와 연결합니다.
2. 재배치: 객체 파일 내의 코드와 데이터를 수정하여, 모든 심볼 참조가 올바른 메모리 주소를 가리키도록 합니다.
3. 실행 파일 생성: 최종 실행 파일을 생성하여 디스크에 저장합니다.
정적 링크가 완료되면, 실행 파일은 완전한 형태로 디스크에 저장되며, 프로그램이 실행될 때 로더에 의해 메모리에 로드됩니다. 이 단계에서는 추가적인 링크 작업이 필요하지 않습니다.
동적 링크는 프로그램이 실행될 때(런타임) 수행됩니다. 이 경우, 일부 라이브러리 함수는 프로그램의 실행 파일에 포함되지 않고, 대신 공유 라이브러리 파일로 별도로 저장됩니다. 동적 링크는 동적 링커(dynamic linker)에 의해 수행되며, 다음 작업을 포함합니다:
.interp
섹션에 지정된 경로에 따라 동적 링커가 호출됩니다.따라서, 링커는 실행 파일을 만들 때만 사용되는 것이 아니라, 동적 링크의 경우 런타임에도 사용되어 실제 메모리 주소를 할당하고 심볼 참조를 해결합니다. 이 동적 링크 덕분에 프로그램이 더 적은 메모리 자원을 사용하고, 라이브러리를 쉽게 업데이트할 수 있는 유연성을 제공합니다.
심볼 해석 규칙
링커를 배워야 하는 이유
큰 프로그램을 작성하는데 도움됨 : 모듈, 라이브러리, 맞지 않는 라이브러리 버전 때문에 링커 에러를 발생시킬 때가 있다. 어떻게 링커가 참조를 해결해나가는지, 라이브러리가 뭔지 모르면 링커 에러날 때 당혹스러울 것.
위험한 에러 회피 : 리눅스 링커가 심볼 참조를 해결할 때 하는 결정들은 프로그램의 정확성에 영향을 조금 줄 수 있다. 전역변수를 중복해서 정의한 프로그램도 기본 설정의 경우 경고 메시지 없이 링커를 통과할 수 있다. 이렇게 되면 런타임 동작 때 혼란스럽게 작동하고 디버깅도 어렵다.
언어의 변수 영역 규칙 구현 이해 : 전역 변수와 지역 변수의 차이는 무엇인가? static을 사용해서 변수나 함수를 정의할 때, 이것은 무슨 의미인가? 등을 알 수 있다.
중요한 시스템 개념 이해 : 링커가 만든 실행 가능 객체 파일은 로딩과 프로그램 실행 같은 중요한 시스템 함수, 가상메모리, 페이징, 메모리 매핑에서 중요한 역할을 하게 됨
공유 라이브러리 이해
대부분의 컴파일 시스템은 사용자를 대신해서 언어 전처리기, 컴파일러, 어셈블러, 링커를 필요에 따라 호출하는 컴파일러 드라이버를 제공한다.
↑ (드라이버가 ASCII 소스 파일에서 예제 프로그램을 실행 목적 파일로 번역할 때 드라이버의 동작 내용) ↑
main.c
를 ASCII 중간 파일인 main.c
로 번역main.i
를 어셈블리어 파일 main.s
로 번역main.s
를 재배치 가능한 바이너리 목적파일 main.o
로 번역prog
을 생성하기 위해 main.o
와 sum.o
을 연결한다linux> ./prog
으로 실행시키면 쉘은 로더(loader)라고 부르는 운영체제 내의 함수를 호출하며, 로더는 실행파일 prog
의 코드와 데이터를 메모리로 복사하고, 제어를 프로그램의 시작 부분으로 전환.컴파일 후에 소스 코드와 라이브러리 코드를 결합해서 단일 실행 파일을 생성하는 과정.
실행 파일이 실행될 때 추가적인 연결이나 로딩작업이 필요하지 않아서 "정적"이라는 용어가 사용됨.
링커가 실행파일 만드는 과정
목적 파일들은 단지 바이트 블록들의 집합임
목적파일 : 컴파일러와 어셈블러가 소스 코드를 기계어로 변환하여 생성하는 파일
목적파일에는 세 가지 형태가 있음
1.
의 특수한 형태. 메모리에 로드되어 동적으로 링크될 수 있음. 일반적으로 공유 라이브러리에 사용됨.현대의 x86-64 리눅스와 유닉스 시스템들은 목적파일 형식으로 Executable and Linkable Format (ELF)을 사용함
ELF 헤더 : 파일의 전체적인 형식을 설명 (ELF 헤더 크기, 목적파일 타입, 머신 타입 등)
섹션 헤더 테이블 : 목적파일의 각 섹션에 대한 정보를 설명
ELF 파일의 주요 섹션
.text
: 컴파일된 프로그램의 기계 코드가 포함됨..rodata
: 읽기 전용 데이터가 포함됨. (ex. printf 문의 형식 문자열, switch 문의 점프 테이블).data
: 초기화된 전역 변수와 정적 변수가 포함됨. 지역 변수는 런타임 스택에 저장되고 .data
나 .bss
섹션에는 안 나타남.bss
: 초기화되지 않은 전역 변수와 정적 변수가 포함됨. 실제 목적파일에서 공간을 차지하진 않음. 위치만 표시함. 런타임 시 이 변수들은 메모리에서 0으로 초기화됨..symtab
: 프로그램에서 정의되고 참조된 함수와 전역 변수에 대한 정보들이 포함됨..rel.text
: .text
섹션에서 다른 객체 파일과 결합될 때 수정되어야 하는 위치 목록.rel.data
: 이 모듈에 의해 정의되거나 참조되는 전역변수들에 대한 재배치 정보..debug
: 프로그램에 정의된 로컬 변수와 typedef, 전역 변수, 원본 C 소스 파일에 대한 디버깅 심볼 테이블. (컴파일러가 -g
옵션으로 호출될 때만 포함됨).line
: 최초 C 소스 프로그램과 .text
섹션 내 기계어 명령어들의 라인 번호들 간의 매핑. (컴파일러가 -g
옵션으로 호출될 때만 포함됨)심볼(Symbols)
심볼은 프로그램에서 함수, 변수, 상수와 같은 엔티티를 나타내는 이름이다.
각 심볼은 프로그램 내에서 고유한 엔티티를 참조하며, 이 엔티티의 위치(주소)와 다른 속성들을 정의한다.
링커가 심볼을 통해 프로그램의 여러 부분을 결합하는데, 이는 다음과 같은 세 가지 유형으로 분류할 수 있다.
지역 링커 심볼들은 지역 프로그램 변수와는 같지 않다.
symtab
에 있는 심볼 테이블은 지역 비정적 프로그램 변수들에 대응되는 심볼을 전혀 포함하지 않음.
이들은 런타임에 스택에 의해 관리되며 링커와는 관련이 없다.
대신, static으로 정의된 지역 변수는 스택이 아닌 .data
나 .bss
섹션에 공간이 할당되고, 심볼 테이블에는 고유한 이름으로 로컬 심볼이 생성된다.
심볼 테이블(Symbol Tables)
각 재배치 가능 목적 모듈 m은 m에 의해서 정의되고 참조되는 심볼들에 대한 정보를 포함하는 심볼 테이블을 갖고 있음.
심볼 테이블은 어셈블러가 컴파일러에서 내보낸 심볼을 사용하여 구축함.
심볼 테이블은 .symtab
섹션에 포함됨. 이 테이블은 각 항목이 배열 형태로 구성되어 있음
들어있는 정보들 : 이름, 값, 크기, 유형(데이터인지 함수인지), 바인딩(지역인지 전역인지)
링커는 자신의 입력 재배치 가능 목적파일들의 심볼 테이블로부터 정확히 한 개의 심볼 정의에 각 참조를 연결시켜서 심볼 참조를 해석.
동일한 모듈 내에 정의된 지역 심볼에 대한 참조를 해석하는 것은 비교적 간단함.
하지만 전역 심볼들에 대한 참조를 해결하는 것은 더 복잡함.
컴파일러가 현재 모듈에서 정의되지 않은 심볼을 발견하면, 해당 심볼이 다른 모듈에서 정의되었을 거라고 가정하고 링커에게 그 심볼의 처리를 맡김. 링커가 해당 심볼을 찾을 수 없으면 에러 메시지를 출력하고 종료함. 예를 들어, 다음 코드를 컴파일하고 링크하려고 할 때,
void foo(void);
int main() {
foo();
return 0;
}
컴파일러는 성공적으로 실행되지만, 링커는 foo
에 대한 정의를 찾을 수 없어 다음과 같은 오류 메시지를 출력한다.
/tmp/ccSz5uti.o: In function ‘main’: undefined reference to ‘foo’
심볼 해석 규칙
컴파일 할 때, 컴파일러는 각 전역 심볼을 어셈블러로 강하게 또는 약하게 보내며, 어셈블러는 이 정보를 재배치 가능 목적파일의 심볼 테이블에 묵시적으로 인코딩한다.
강한 심볼 : 함수들과 초기화된 전역변수들
약한 심볼 : 비초기화된 전역변수
링커가 재배치 가능 객체 파일들을 읽어들이고, 이를 결합하여 출력 실행 파일을 생성할 때 정적 라이브러리를 사용하면 필요한 함수들만 사용자들에게 제공해서 용량을 줄일 수 있다.
정적 라이브러리는 관련 객체 모듈들을 하나의 파일로 패키징한 것으로, 링커가 이 파일을 입력으로 받아서 프로그램에 필요한 객체 모듈만을 복사하여 사용한다.
정적 라이브러리를 사용할 때 링커가 이에 대한 참조를 해석하면서 혼란이 발생하기도 한다.
링커는 심볼 해석 단계에서 컴파일러 드라이버의 명령줄에 나타난 순서대로 재배치 가능 객체 파일과 아카이브를 왼쪽에서 오른쪽으로 순서대로 스캔한다. 이 과정에서 링커는 세 개의 집합을 관리한다.
과정이 이러하기 때문에, 라이브러리가 객체 파일 앞에 나타나면 참조가 해결되지 않아 링크가 실패할 수 있다.
gcc -static ./libvector.a main2.c
이러면 링크 안됨
gcc main2.c -L. -lvector
라이브러리를 명령줄 끝에 배치하면 링크 됨
라이브러리들 간의 의존성이 있는 경우, 의존성을 만족시키기 위해 명령줄에 라이브러리를 여러 번 반복해서 나타내야 할 수도 있다.
재배치 : 링커가 심볼 해석을 완료한 후, 각 심볼 참조를 정확히 하나의 심볼 정의와 연결하는 과정. 입력 모듈들을 합치고 각 심볼에 런타임 주소를 할당한다. 이 때 링커는 입력 목적 모듈들 안에 코드와 데이터 섹션들의 정확한 크기를 알고 있다.
재배치 과정은 두 가지 단계로 구성된다
1. 섹션 및 심볼 정의 재배치
이 단계에서 링커는 같은 종류의 모든 섹션을 새로운 집합 섹션으로 합친다.
예를 들어, 입력 모듈들의 .data
섹션들은 출력 실행 목적파일을 위한 한 개의 .data
섹션으로 합쳐진다.
그 후 런타임 메모리 주소를 새로운 통합된 섹션들, 입력 모듈들에 의해 정의된 각 섹션들, 입력 모듈들에서 정의된 각 심볼들에 할당한다.
이 단계가 완료되면 프로그램의 각 명령어와 전역 변수가 고유한 런타임 메모리 주소를 갖게 된다.
2. 섹션 내 심볼 참조 재배치
링커가 코드와 데이터 섹션들 내의 모든 심볼 참조들을 수정해서 이들이 정확한 런타임 주소를 가리키도록 한다.
이 단계를 수행하기 위해서 링커는 재배치 가능 목적 모듈의 '재배치 엔트리'라는 자료구조를 사용한다.
어셈블러가 객체 모듈을 생성할 때, 코드와 데이터가 궁극적으로 메모리의 어디에 저장될 지 알 수 없다.
또한, 모듈이 참조하는 외부 정의 함수나 전역 변수의 위치도 모른다.
따라서 어셈블러는 참조의 궁극적인 위치를 알 수 없을 때마다 해당 참조를 수정하는 방법을 설명하는 재배치 항목을 생성한다.
코드의 재배치 항목은 .rel.text
섹션에, 데이터의 재배치 항목은 .rel.data
섹션에 배치된다.
앞서 설명한 것들은 링커가 다수의 목적파일들을 하나의 실행 가능 목적파일로 합치는 과정이었다.
이제 C 프로그램이 ASCII 텍스트 파일 모음에서 시작해서,
메모리에 로드하고 실행하는데 필요한 모든 정보를 포함하는 단일 바이너리 파일로 변환되었다.
실행 파일의 구조 (ELF 형식)
.text
, .rodata
, .data
섹션 : 재배치 가능한 객체 파일과 유사하지만, 링커에 의해 최종적인 런타임 주소로 재배치된다..init
섹션 : 프로그램의 초기화 코드에 의해 호출되는 작은 함수 _init
을 정의한다..rel
섹션은 없다. 실행 파일이 완전히 링크되었기 때문에, 추가적인 재배치 정보 섹션이 필요하지 않기 때문이다.ELF 실행 파일
ELF 실행 파일은 메모리에 쉽게 로드될 수 있도록 설계되었다.
즉, 실행 파일의 연속적인 청크들이 연속적인 메모리 세그먼트에 매핑된다.
프로그램 헤더 테이블 : 실행 파일이 메모리에 어떻게 매핑될지를 설명. 코드 세그먼트와 데이터 세그먼트로 나뉨.
코드 세그먼트 : 실행 파일의 기계어 명령어를 포함한다.
- 명령어 코드 : 컴파일러에 의해 생성된 프로그램의 기계어 명령어
_init
함수).rodata
섹션)데이터 세그먼트 : 초기화된 전역 변수와 정적 변수를 포함한다.
- 초기화된 데이터 : 프로그램 시작 시 특정 값으로 초기화된 전역 변수와 정적 변수 (.data
섹션)
.bss
섹션)사용자가 터미널에서 실행 파일을 실행하려고 할 때, 예를 들어 ./prog
를 입력하면, prog가 내장 셸 명령어에 대응되지 않기 때문에 셸은 해당 파일이 실행 가능한 목적 파일이라고 가정하고 이를 실행하려고 시도한다. 셸은 로더를 호출하여 이 작업을 수행한다.
로더는 디스크에서 실행 파일을 메모리에 로드한다. 이 때, 프로그램 헤더 테이블을 참조하여 실행 파일의 각 세그먼트를 메모리의 적절한 위치에 복사한다. 로더는 먼저 코드와 데이터 세그먼트를 메모리에 복사한 후, 프로그램의 진입점으로 점프하여 프로그램 실행을 시작한다.
이 진입점은 보통 _start
함수로, 시스템 초기화 루틴을 호출하고, 사용자 프로그램의 main
함수를 호출하는 역할을 한다.
이와 같이 프로그램을 메모리로 복사하고 실행하는 과정을 로딩이라고 부른다.
로딩 과정
_init
함수와 같은 초기화 코드를 실행한다.런타임 메모리 이미지
실행 파일이 메모리에 로드되면, 각 프로그램은 런타임 메모리 이미지를 갖게 된다.
제어 흐름의 양상은 크게 세 종류이다.
예외적인 제어 흐름(ECF)은 시스템 상태의 갑작스러운 변화에 대응하여 프로그램의 실행 흐름이 변화하는 것을 의미한다.
ECF는 하드웨어, 운영체제, 응용 프로그램 수준에서 모두 발생할 수 있으므로 이에 대한 대응도 전부 준비되어 있어야 한다.
ECF를 이해해야 하는 이유
시스템 개념 이해 : ECF는 운영체제가 입출력, 프로세스, 가상메모리를 구현하기 위해 사용하는 기본 메커니즘임
응용 프로그램과 운영체제의 상호작용 이해 : 응용 프로그램은 트랩(trap) 또는 시스템 콜(system call)이라고 알려진 ECF를 이용해서 운영체제로부터 서비스를 요청함. 기본 시스템 콜 메커니즘을 이해하면 데이터를 디스크에 쓰거나, 네트워크에서 데이터를 읽는 등의 서비스들이 어떻게 응용 프로그램에 제공되는지 이해하는 데 도움됨
재미있는 새로운 응용프로그램 작성 : 새로운 프로세스를 만들거나, 프로세스가 종료하기를 기다리거나, 다른 프로세서에게 시스템 내의 예외 이벤트를 알리거나, 이러한 이벤트를 감지하고 반응하는 등의 작업을 위한 ECF 메커니즘은 운영체제가 응용프로그램들에게 제공한다. ECF 메커니즘들을 이용해야 Unix 쉘과 웹 서버 같은 흥미로운 프로그램을 작성할 수 있다.
동시성 : ECF는 컴퓨터 시스템에서 동시성을 구현하는 기본 메커니즘임. 실행 시간이 중첩되는 프로세스, 쓰레드, 응용프로그램의 실행을 가로채는 예외처리 핸들러, 응용프로그램의 실행을 가로채는 시그널 핸들러 등...
소프트웨어적인 예외의 동작 이해 : C++과 자바 같은 언어는 try, catch, throw 문장을 통해서 소프트웨어 예외 메커니즘을 제공함. 소프트웨어 예외는 프로그램이 에러 발생 시에 비지역성(nonlocal) 점프(즉, 일반적인 call/return 스택 방식에 위배되는 점프)를 하도록 해준다. 비지역성 점프는 응용 수준의 ECF이며, C에서는 setjmp, longjmp 함수로 제공된다. 이러한 저수준 함수들을 이해하면 소프트웨어 수준의 고수준 예외를 이해할 수 있게됨.
앞 장까지는 응용이 하드웨어와 어떻게 상호작용하는지 배우는 거였고,
이번 장은 응용이 운영체제와 어떻게 상호작용하는지 배우기 시작한다.
이러한 상호작용들은 모두 ECF를 중심으로 돌아간다.
예외 : 프로세서 상태의 변화에 대한 대응이며, 제어흐름의 갑작스런 변화.
프로세서가 어떤 명령어를 실행하고 있을 때 프로세서 상태에 변화가 일어나면(명령어와 관련 있을수도 있고 없을수도 있음) 예외 테이블이라는 점프 테이블을 통해서 이 특정 종류의 이벤트를 처리하기 위해 특별히 설계된 운영 체제 서브루틴(예외처리 핸들러)으로 간접 프로시저 콜을 하게 된다.
예외처리 핸들러가 처리를 끝마치면, 예외상황을 발생시킨 이벤트의 종류에 따라서 세 가지 중 하나가 발생한다.
예외 처리 : 예외가 발생했을 때 운영체제와 하드웨어가 이를 처리하는 과정
예외 번호 : 가능한 예외 상황들에 중복되지 않는 양의 정수를 예외 번호로 할당함
예외 처리 과정
일반적인 함수의 호출과 예외 핸들러의 차이점
발생 원인과 처리 방식에 따라 분류한 예외 종류 (책마다 정의 다름)
인터럽트 (Interrupts)
트랩 (Traps)
폴트 (Faults)
어보트 (Aborts)
프로세스는 실행 중인 프로그램의 인스턴스를 의미함.
운영체제는 여러 프로세스를 동시에 실행시켜 사용자에게 여러 작업이 동시에 수행되는 것처럼 보이게 함.
각각의 프로세스는 특정 문맥에서 실행됨.
문맥(Context) : 프로그램이 올바르게 실행되기 위해 필요한 상태 정보들의 집합
사용자가 실행 목적파일의 이름을 쉘에 입력해서 프로그램을 돌릴 때마다 쉘은 새로운 프로세스를 생성하고, 실행 목적파일을 이 새로운 프로세스의 문맥에서 실행함.
프로세스는 두 가지 중요한 추상화를 제공함.
독립적인 논리적 제어 흐름 (Independent Logical Control Flow)
: 프로세서는 프로그램 내의 명령어들을 차례대로 중단 없이 실행하는 것처럼 보이고,
개인 주소 공간 (Private Address Space)
: 프로그램의 코드와 데이터는 시스템 메모리 상의 유일한 객체인 것처럼 보임.
프로세스는 시스템에 서로 다른 여러 프로그램들이 동시적으로 동작하고 있지만, 프로세서를 혼자서 사용한다는 착각을 느끼게 한다.
디버거 써보면 명령어들에 PC (프로그램 카운터) 값들이 대응된다는 걸 알 수 있음.
이러한 PC 값들의 배열을 논리적 제어흐름 또는 간단히 논리흐름이라고 부름.
하나의 프로세서에는 여러 프로세스들이 교대로 돌아감.
각 프로세스는 자신의 흐름의 일부를 실행한 후 다른 프로세스들이 실행되는 동안 일시적으로 정지된다.
논리흐름은 컴퓨터 시스템 내에서 여러 가지 다른 형태를 가짐.
예외 핸들러, 프로세스, 시그널 핸들러, 쓰레드, 자바 프로세스 등...
동시성 흐름 : 자신의 실행시간이 다른 흐름과 겹치는 논리흐름. 이 두 흐름은 동시에 실행한다고 표현함.
동시성 : 여러 개의 논리적 흐름이 동시적으로 실행되는 것
멀티태스킹 : 프로세스가 다른 프로세스들과 교대로 실행되는 것. 타임 슬라이싱이라고도 함.
타임 슬라이스 : 한 프로세스가 자신의 흐름 일부를 실행하는 매 시간 주기
동시적 흐름은 프로세서 코어나 컴퓨터 개수와는 무관함. 두 흐름이 시간상으로 중첩되면, 이들이 동일한 프로세서에서 돌아가고 있더라도 이들은 동시적. 두 개의 흐름이 서로 다른 프로세서 코어나 컴퓨터에서 동시에 돌아가고 있다면 이건 병렬 흐름.
프로세스는 각 프로그램에 사적 주소 공간을 제공함으로써 자신이 시스템의 주소공간을 혼자서 사용한다는 착각을 불러일으킨다.
사적 주소 공간의 특정 주소에 연결된 메모리의 한 개의 바이트가 일반적으로 다른 프로세스에 의해서 읽히거나 쓰일 수 없기 때문에, 이 공간은 사적(Private)이다.
각각의 사적 주소공간에 저장된 내용은 서로 다르더라도, 구조는 동일하다.
프로세서는 응용프로그램이 실행할 수 있는 명령어들을 제한해야 한다.
프로세서는 이를 위해 프로세스가 현재 가지고 있는 특권(Privilege)을 저장하는 일부 제어 레지스터로 모드 비트를 사용한다.
모드 비트가 설정되면 프로세스는 커널 모드로 동작한다. 커널 모드에서 돌고 있는 프로세스는 모든 명령어를 사용할 수 있고, 시스템 내의 어떤 메모리 위치도 접근할 수 있다.
모드 비트가 없으면 프로세스는 사용자 모드에서 돌아간다. 사용자 모드의 프로세스는 프로세서를 멈추거나, 모드 비트를 변경하거나, 입출력 연산을 초기화하는 등의 특수 인스트럭션을 실행할 수 없다. 이러한 시도를 하면 보호 오류가 발생한다. 그 대신 사용자 프로그램은 시스템 콜을 통해서 커널 코드와 데이터에 간접적으로 접근해야 한다.
프로세스가 사용자 모드에서 커널 모드로 진입하는 유일한 방법은 인터럽트, 오류같은 예외를 통해서다. 예외 핸들러에서 돌아오면 다시 유저 모드가 된다.
운영체제 커널은 문맥 전환이라는 ECF의 상위수준 형태를 사용해서 멀티태스킹(프로세스가 다른 프로세스들과 교대로 실행되는 것)을 구현하고 있다. 문맥 전환은 ECF에 해당하는 예외 매커니즘을 기반으로 구현된다.
문맥 : 커널이 중단됐던 프로세스를 다시 시작하기 위해서 필요로 하는 상태 정보들이다. 범용 레지스터, 프로그램 카운터, 상태 레지스터, 유저 스택, 커널 스택 등의 정보들로 구성된다.
스케줄링 : 프로세스가 실행되는 동안 어떤 시점에 현재 프로세스를 정지하고 다른 프로세스를 다시 시작할지 결정하는 것. 스케줄러라고 불리는 커널 내부의 코드에 의해 처리됨. 커널이 실행할 새 프로세스를 선택하면 그 프로세스는 커널에 의해 스케줄된 것.
문맥 전환 : 커널이 실행할 새 프로세스를 스케줄한 후에 현재 프로세스를 정지하는 것
문맥 전환은 커널이 사용자를 대신해서 시스템 콜을 실행하고 있을 때 일어날 수 있다. 만약 어떤 시스템 콜이 특정 이벤트의 발생을 기다리고 있다면, 커널은 현재 프로세스를 sleep시키고 다른 프로세스로 문맥 전환한다.
문맥 전환은 인터럽트에 의해 발생할 수도 있다. 예를 들어, 모든 시스템은 대개 1 ms 또는 10 ms마다 주기적인 타이머 인터럽트를 생성하는 어떤 메커니즘을 갖고 있다. 타이머 인터럽트가 일어날 때마다 커널은 현재 프로세스가 충분히 오래 실행됐다고 판단하고 문맥 전환한다.
N, M = map(int, input().split())
arr = [i for i in range(1,N+1)]
def permutation(arr, case):
if len(arr) == N-M:
for n in case:
print(n, end=' ')
print()
return
for i in range(len(arr)):
case.append(arr[i])
permutation(arr[:i]+arr[i+1:len(arr)],case)
case.pop()
permutation(arr,[])
똑같은거 풀었던 기억으로 금방 풀긴 했는데 사실상 답을 기억해내서 푼거라 이게 맞나? 싶긴함. 근데 안 푼 것보단 낫지 않나 싶음.