컴퓨터 시스템 => 하드웨어 + 시스템 소프트웨어
사용자 (User)
│
▼
응용 소프트웨어 (Application Software)
│ 예) 웹 브라우저, 게임, 문서 편집기
▼
운영체제 (Operating System, OS)
│ 예) Windows, Linux, macOS, Android
│
├── 프로세스 관리 (CPU 스케줄링)
├── 메모리 관리 (RAM 할당)
├── 파일 시스템 (HDD/SSD 저장)
├── 네트워크 관리 (인터넷 연결)
│
▼
드라이버 & 펌웨어 (Drivers & Firmware)
│ 예) 그래픽 카드 드라이버, 키보드 드라이버
▼
하드웨어 (Hardware)
├── CPU (명령어 실행)
├── RAM (데이터 저장)
├── HDD/SSD (파일 저장)
├── GPU (그래픽 처리)
├── 네트워크 장치 (인터넷 연결)
hello world를 출력하는 모든 과정을 따라가는 것으로 우리는 컴퓨터 시스템에 대한 이해도를 올릴 수 있다. hello.c에서
#include <stdio.h>
int main()
{
prinf("hello, world\n");
return 0
}
이런 코드를 작성했을 때 실행 결과를 예측할 수 있을 것이다. 하지만 이 프로그램 또한 프로그래머에 의해 만들어졌고, 시스템이 실행되고, 문자열이 출력되고, 프로그램이 종료될 때까지의 주기를 아는 것은 별개의 문제다.
📌 주요 흐름
1️⃣ 컴파일 과정 (전처리 → 컴파일 → 어셈블 → 링킹)
2️⃣ 프로그램 실행 (쉘 → 커널 → 프로세스 생성)
3️⃣ printf() 실행 (시스템 호출 → 출력 버퍼 → 터미널)
4️⃣ 프로그램 종료 (메모리 해제 → 쉘로 복귀)
[ 시작 ]
│
▼
(1) C 소스 코드 작성 (`hello.c`)
│
▼
(2) 컴파일 (`gcc hello.c -o hello`)
│
├── [전처리] `#include <stdio.h>` 처리
├── [컴파일] C → 어셈블리 변환
├── [어셈블] 어셈블리 → 기계어 변환
├── [링킹] `printf()` 등 라이브러리 연결
▼
(3) 실행 파일 생성 (`hello`)
│
▼
(4) 쉘에서 실행 (`./hello`)
│
▼
(5) 리눅스 커널이 `fork()` 실행 (새 프로세스 생성)
│
▼
(6) `execve()` 로 실행 파일 로드
│
▼
(7) `main()` 함수 실행
│
▼
(8) `printf("Hello, World!\n")` 실행
│
├── `printf()` 가 내부적으로 `write()` 시스템 호출
├── 커널이 `stdout`(터미널)으로 데이터 전달
├── 출력 버퍼를 통해 터미널에 "Hello, World!" 표시
▼
(9) `return 0;` 실행 → `exit()` 호출
│
▼
(10) 프로세스 종료 & 메모리 해제
│
▼
(11) 쉘이 다시 사용자 입력 대기 (`$` 프롬프트 표시)
│
▼
[ 종료 ]
더 알아보기
hello.c -> 전처리 과정 -> hello.i -> C 코드를 어셈블리 코드로 변환 -> hello.s -> 어셈블리 코드 (hello.s) 를 기계어(바이너리 코드) 로 변환 -> hello.o
1️⃣ fork() 호출 → 새로운 프로세스를 생성 (부모-자식 관계).
2️⃣ execve() 호출 → 실행 파일을 메모리에 로드.
3️⃣ 프로세스 주소 공간 할당
코드 영역 → hello 실행 파일의 바이너리 코드 로드.
데이터 영역 → 전역 변수 저장.
힙(Heap) → 동적 메모리 할당 공간.
스택(Stack) → 함수 호출 정보 저장.
write(1, "Hello, World!\n", 14);
파일 디스크립터 1 (STDOUT) → 터미널 출력
"Hello, World!\n" → 출력할 문자열
14 → 문자열 길이
1️⃣ write() 함수 호출
2️⃣ 리눅스 커널의 시스템 호출 인터페이스로 진입
3️⃣ 커널이 문자열을 출력 버퍼(output buffer) 에 저장
4️⃣ 터미널이 출력 버퍼에서 데이터를 가져와 화면에 출력
커널에 프로세스 종료 요청
프로세스가 사용하던 메모리 반환
부모 프로세스 (쉘) 에게 종료 코드(0) 전달
쉘 프롬프트($)가 다시 나타남
전처리 과정은 C 프로그램이 컴파일되기 전에 수행되는 단계로, #include, #define, #ifdef 등의 전처리 지시문을 처리하는 역할을 해.
[ 시작 ]
│
▼
(1) C 소스 코드 작성 (`hello.c`)
│
▼
(2) 전처리기(`gcc -E`) 실행
│
├── `#include` 처리 → 헤더 파일 삽입
├── `#define` 처리 → 매크로 치환
├── `#ifdef` 처리 → 조건부 컴파일 수행
├── 주석 제거
▼
(3) 전처리된 코드 (`hello.i`) 생성
│
▼
(4) 컴파일러가 `hello.i` 를 받아서 컴파일 진행
│
▼
[ 전처리 완료 후 컴파일 단계로 이동 ]
C 코드를 어셈블리 코드로 변환하는 이유 :
CPU가 이해할 수 있는 코드로 변환하기 위해.
C 코드는 사람이 이해하기 쉬운 고수준 언어지만, CPU는 오직 기계어(0과 1) 만 이해해.
C → 어셈블리 → 기계어 변환 과정이 필요함.
커널에 실행 요청을 보내는 방법 :
시스템 콜을 통해서 사용자 공간에서 커널 공간으로 요청을 보낼 수 있다.
[ 사용자(쉘) 입력 ]
│
▼
[ execve() 시스템 호출 ] # 실행 파일 요청
│
▼
[ 리눅스 커널 ]
│
├── ① 프로세스 생성 (fork())
├── ② 실행 파일 로드 (execve())
├── ③ 메모리 할당 (가상 메모리 설정)
├── ④ CPU 스케줄링 (실행 준비)
▼
[ 프로그램 실행 시작 ]
execve() 시스템 호출
#include <unistd.h>
int main() {
char *args[] = { "/bin/ls", NULL };
execve("/bin/ls", args, NULL);
return 0;
}
🔹 위 코드는 /bin/ls 실행 요청을 커널에 전달하는 역할을 해.
🔹 커널은 실행 파일을 로드하고 새로운 프로세스를 생성해 실행시켜 줘.
실행 과정
1️⃣ 사용자 프로그램이 execve() 호출
2️⃣ 커널이 실행 파일을 찾아 ELF 바이너리를 로드
3️⃣ 새로운 프로세스 주소 공간 생성
4️⃣ 기존 프로세스를 새로운 실행 파일로 덮어쓰기
5️⃣ 실행 파일 시작 (main() 실행)
새로운 프로세스 생성
운영체제(OS)가 실행 중인 프로그램의 복사본을 만들어 독립적인 실행 흐름을 제공하는 것을 의미
이 말은 즉 우리가 터미널에 명령어를 치면 터미널은 시스템 콜로 커널 공간으로 요청을 보내면 커널은 실행 파일을 읽고 새로운 프로세스를 생성 -> 어떤 메모리 공간에 할당한다는 뜻인 되는건가?
실행 중인 프로세스를 복제하여 새로운 프로세스를 생성하는 시스템 호출.
[ 부모 프로세스 실행 중 ]
│
▼
[ fork() 호출 ]
│
├── 부모 프로세스 (PID 유지)
└── 자식 프로세스 (새로운 PID 할당)
│
▼
[ 부모 & 자식 프로세스 독립적으로 실행 ]
fork()를 사용해서 시스템을 호출하면 부모 프로세스와 자식 프로세스가 생성됨. -> 부모 프로세스와 자식 프로세스는 서로 독립적이므로 영향을 받지 않는다. -> 그러므로 동시에 여러개의 프로그램을 돌릴 수 있다.
✅ 멀티태스킹 지원 → 여러 프로그램을 동시에 실행 가능 (firefox, chrome, ls 등)
✅ 독립적인 작업 수행 → 하나의 프로그램에서 여러 작업을 동시에 실행 가능
✅ 백그라운드 작업 수행 → 서버에서 여러 클라이언트 요청을 동시에 처리 가능
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork(); // 새로운 프로세스 생성
if (pid == 0) {
// 자식 프로세스
printf("나는 자식 프로세스입니다! (PID: %d)\n", getpid());
} else {
// 부모 프로세스
printf("나는 부모 프로세스입니다! (PID: %d, 자식 PID: %d)\n", getpid(), pid);
}
return 0;
}
나는 부모 프로세스입니다! (PID: 1234, 자식 PID: 1235)
나는 자식 프로세스입니다! (PID: 1235)
부모와 자식이 독립적으로 실행함
#include <stdio.h>
#include <unistd.h>
int main() {
pid_t pid = fork();
if (pid == 0) {
// 자식 프로세스: 새로운 프로그램 실행
char *args[] = { "/bin/ls", NULL };
execve("/bin/ls", args, NULL);
} else {
// 부모 프로세스
printf("부모 프로세스입니다! (PID: %d, 자식 PID: %d)\n", getpid(), pid);
}
return 0;
}
부모 프로세스입니다! (PID: 1234, 자식 PID: 1235)
Desktop Documents Downloads Music Pictures Videos
공부 중에 잘 보고갑니다. 같이 힘내봐요