Skip to content

Commit

Permalink
Merge pull request #10 from SSUMC-6th/bey/#1
Browse files Browse the repository at this point in the history
[베이] Chapter 1. 서버란 무엇인가(소켓&멀티 프로세스)
  • Loading branch information
h-ye-ryoung authored Apr 10, 2024
2 parents 4958897 + 0e256ae commit cc31478
Show file tree
Hide file tree
Showing 11 changed files with 556 additions and 0 deletions.
53 changes: 53 additions & 0 deletions docs/chapter1/Ch01Keyowrd_03.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
Keyword: 시스템 콜

> 운영 체제의 커널이 제공하는 서비스에 대해,
응용 프로그램의 요청에 따라 커널에 접근하기 위한 인터페이스

ex) 사용자 프로그램이 디스크 파일에 접근해야 할 경우,
운영체제에게 특권 명령의 대행을 요청하는 것
>

### 시스템 콜을 호출한다면

1. 각 시스템 콜에는 번호가 할당되고 시스템 콜 인터페이스는 **시스템 콜 번호와 시스템 콜 핸들러 함수 주소로 구성되는 시스템 콜 테이블**을 유지한다.
2. 운영체제는 자신의 커널 영역에서 해당 인덱스가 가리키는 주소에 저장되어 있는 루틴을 수행한다.
3. 작업이 완료되면 CPU에게 인터럽트를 발생시켜 수행이 완료 되었음을 알린다.

<aside>
💡 만약 시스템 콜이 발생했을 때 OS로 전달할 추가적인 정보가 필요할 경우

정보가 담긴 매개변수들을 메모리에 저장해 그 메모리의 주소를 레지스터에 전달하거나, 프로그램에 의해 스택에 전달된다.

</aside>

### 시스템 콜 종류

프로세스/스레드 관련 (생성 또는 kill)

파일 I/O 관련 (읽기, 쓰기, 읽었는지 확인..)

소켓 관련 (네트워크 관련 작업)

장치 관련 (키보드 입력 등)

프로세스 통신 관련

### 시스템 콜이 필요한 이유

- 일반적으로 사용하는 ‘응용 프로그램’ → 유저 레벨의 프로그램
- 유저 레벨의 함수들만으로는 많은 기능을 구현하기 힘들어서, `커널`의 도움을 받아야 함
- 이 작업은 유저 모드(=유저 프로세스, 즉 응용프로그램) 에서는 수행할 수 없고,

반드시 **커널에 관련된 것은 커널 모드로 전환한 후에야** 해당 작업을 수행할 권한이 생긴다.


| 유저 모드 : PC register 가 사용자 프로그램이 올라가 있는 메모리 위치를 가리키고 있을 때 → CPU가 유저 모드에서 수행중 |
| 커널 모드 : PC register 가 운영체제가 존재하는 부분을 가리키고 있다면 → 운영체제의 코드 수행중, CPU가 커널모드에서 수행중 |


모드 비트 : CPU 내에 모드 비트를 두어서 구분
일반 명령: 1, 특권 명령: 0


| 일반 명령(유저 모드) : 메모리에서 자료를 읽어와서 CPU에서 계산하고 결과를 메모리의 쓰는 일련의 명령들 → 모든 프로그램이 수행 가능
| 특권 명령 (커널 모드) : 보안이 필요한 명령, 입출력 장치, 타이머 등 각종 장치에 접근하는 명령
22 changes: 22 additions & 0 deletions docs/chapter1/Ch01Keyword._05txt.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
Keyword: 리눅스의 파일과 파일 디스크립터

### 파일 디스크립터

> 프로세스에서 열린 파일의 목록을 관리하는 테이블의 `‘인덱스’`
시스템으로부터 할당받은 파일을 대표하는 `0이 아닌 정수 값`
>

**유닉스 시스템에서 모든 것은 파일이다.** (Socket, 내/외부 모든 장치도 파일)

**유닉스에서 프로세스가 이 파일들을 접근할 때 File Descriptor라는 개념을 이용한다.**

각 파일 디스크립터는 어떤 정수값인데, 그것은 어떤 하나의 open file과 연결되어 있다.

그리고 프로세스들은 파일 디스크립터를 이용해 데이터를 처리한다.

### 파일 디스크립터 테이블

- 각각의 프로세스는 파일 디스크립터 테이블을 가진다.
- 파일 디스크립터 테이블에는 파일 디스크립터가 저장되어 있다.
- 파일 디스크립터 테이블은 프로세스가 생성될 때 기본적으로 0, 1, 2에 해당하는 파일 디스크립터가 매핑된다.
- 프로세스가 실행중인 파일을 Open하면 커널은 해당 프로세스의 파일 디스크립터 숫자 중에 사용하지 않는 가장 작은 값을 할당해준다.
47 changes: 47 additions & 0 deletions docs/chapter1/Ch01Keyword_01.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
keyword : TCP

> TCP/IP 프로토콜의 `전송 계층`에서 사용되는 프로토콜, IP 프로토콜 기반 구현
(전송 계층: IP에 의해 전달되는 패킷의 오류를 검사하고 재전송 요구 등의 제어를 담당하는 계층)

데이터 송신이 `양방향`이고, 신뢰성이 요구되는 애플리케이션에 사용
>

### TCP의 특징

- `연결형 서비스` (3-way handshaking, 4-way handshaking)
- `흐름제어` → 데이터 처리 속도를 조절
- 송신하는 곳에서 매우 빠르게 데이터를 보내서 문제가 생기지 않도록, 수신자가 수신량을 정할 수 있음
- `혼잡제어` → 데이터의 양을 조절
- 패킷의 수가 넘치지 않도록, 정보가 과다하면 패킷을 조금만 전송
- `신뢰성` → ACK을 통한 이상 감지
- 순서대로 데이터 전송

### ACK 제어비트

- 송신측에 대해 **수신측에서 긍정 응답으로** 보내지는 전송 제어용 char
- ACK 번호를 사용하여 패킷이 도착했는지 확인
- 패킷이 제대로 도착하지 않았다면 재송신을 요구

### 3-way handshaking

**TCP 연결을 설정하는 과정 (SYN과 ACK 전달)**

1. 먼저 open()을 실행한 클라이언트가 `SYN`을 보내고 `SYN_SENT` 상태로 대기
2. 서버는 `SYN_RCVD` 상태로 바꾸고 `SYN`과 응답 `ACK`를 보냄
3. `SYN`과 응답 `ACK`을 받은 클라이언트는 `ESTABLISHED` 상태로 변경하고 서버에게 응답 `ACK`를 보냄
4. 응답 `ACK`를 받은 서버 또한 `ESTABLISHED` 상태로 변경

`ESTABLISHED` : 연결 완료

### 4-way handshaking

**TCP 연결을 해제하는 과정 (FIN과 ACK 전달)**

1. 먼저 close()를 실행한 클라이언트가 `FIN`을 보내고 `FIN_WAIT1` 상태로 대기
2. 서버는 `CLOSE_WAIT`으로 바꾸고 응답 `ACK`를 전달.
동시에 해당 포트에 연결되어 있는 어플리케이션에게 close()를 요청
3. ACK를 받은 클라이언트는 상태를 `FIN_WAIT2`로 변경한다.
4. close() 요청을 받은 서버 어플리케이션은 종료 프로세스를 진행하고 `FIN`을 클라이언트에 보내 `LAST_ACK` 상태로 바꾼다.
5. FIN을 받은 클라이언트는 ACK를 서버에 다시 전송하고 `TIME_WAIT`으로 상태를 바꾼다. `TIME_WAIT`에서 일정 시간이 지나면 `CLOSED`된다. ACK를 받은 서버도 포트를 `CLOSED`로 닫는다.

`CLOSED` : 연결 해제 완료
12 changes: 12 additions & 0 deletions docs/chapter1/Ch01Keyword_02.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
keyword : UDP

> TCP/IP 프로토콜의 `전송 계층`에서 사용되는 프로토콜, IP 프로토콜 기반 구현
(전송 계층: IP에 의해 전달되는 패킷의 오류를 검사하고 재전송 요구 등의 제어를 담당하는 계층)

데이터 송신이 `단방향`이고, 간단한 데이터를 빠르게 전달할 때 사용
>

### UDP의 특징

- `비연결지향` : 단방향 송신
- 순서가 보장되지 않는다.
41 changes: 41 additions & 0 deletions docs/chapter1/Ch01Keyword_04.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
Keyword: 하드웨어 인터럽트

> 인터럽트:
시스템에서 발생한 다양한 종류의 이벤트, 혹은 그런 이벤트를 알리는 매커니즘

인터럽트가 발생하면 CPU에서는 인터럽트 처리를 위해
**즉각적으로 커널 모드에서 커널 코드**를 실행

즉, 언제든지 인터럽트가 발생하는 순간 실행 중이던 사용자 프로그램을 마무리한 후,
인터럽트 처리를 위해 커널이 동작권을 넘겨받아서 관련 처리를 하게 됨
>

### 인터럽트 종류

- 전원(power)에 문제가 생겼을 때
- I/O 작업이 완료됐을 때
- 시간이 다 됐을 때(timer 관련)
- 유저 레벨에서 발생하는 inturrupt : `TRAP`
- 0으로 나눴을 때
- 잘못된 메모리 공간에 접근을 시도할 때 …etc

### 하드웨어 인터럽트

하드웨어가 발생시키는 인터럽트로, CPU가 아닌 다른 하드웨어 장치가 CPU에 어떤 사실을 알려주거나 CPU 서비스를 요청해야 할 경우 발생시킨다.

### 소프트웨어 인터럽트

소프트웨어가 발생시키는 인터럽트로, 소프트웨어(사용자 프로그램)가 스스로 인터럽트 라인을 세팅한다.

- ex) 예외 상황(Exception), system call

### 인터럽트 동작 과정

1. 프로그램 실행 중에 인터럽트가 발생하거나 시스템 콜을 호출하게 되면 유저 모드 → 커널 모드로 전환
2. 커널 모드에서, 방금 전까지 실행 중이던 프로그램의 현재 CPU 상태를 저장함 (나중에 이어서 실행할 수 있도록)
- 현재까지 수행중이었던 상태를 해당 process의 **PCB(Process Control Block)**에 저장
- PC(Program Counter)에 다음에 실행할 명령의 주소를 저장
3. 커널이 발생했던 인터럽트나 시스템 콜을 직접 처리. 즉, CPU에서 ‘커널 코드’가 실행됨
4. 모든 처리가 완료되면 중단됐던 프로그램의 CPU 상태를 복원
5. 다시 통제권을 프로그램에게 반환 (커널 모드→유저 모드)
6. PC값을 복원하여 프로그램이 이어서 실행됨 (유저 모드)
31 changes: 31 additions & 0 deletions docs/chapter1/Ch01Keyword_06.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
Keyword: socket() 시스템 콜

### socket(domain, type, protocol);

> **미리 소켓의 형태를 잡아두는 시스템 콜**
리턴 값: 파일 디스크립터
>

<aside>
💡 서버와 클라이언트 모두 호출

</aside>

리눅스에서는 모든 것을 파일로 취급하며 소켓 역시 파일로 취급한다.

### Socket() 리턴 값 → 파일 디스크립터

웹서버 프로세스가 데이터를 전송하기 위해 write(), read() 시스템 콜을 사용 할 때,
**리턴된 소켓의 파일 디스크립터를 파라미터로 전송**하여
OS에게 어떤 파일에 데이터를 작성할지, 혹은 어떤 파일의 데이터를 요청할지 결정함

→ 즉 return된 디스크립터로 **소켓에 데이터를 작성(데이터 송신)하거나,**

**소켓의 데이터를 읽어들이는(데이터 수신) 동작을 진행**

### Socket() 파라미터

Socket의 파라미터를 통해,

Ipv4 통신을 위해 사용할지, Ipv6 통신을 위해 사용할지,
TCP를 사용할지 아니면 UDP를 사용할지 미리 **틀을 만들어둔다.**
23 changes: 23 additions & 0 deletions docs/chapter1/Ch01Keyword_07.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
Keyword: bind() 시스템 콜

### bind(***sockfd, sockaddr, socklen_t);***

> **생성한 소켓에 실제 아이피 주소와 포트번호를 부여하는 시스템 콜**
리턴 값: X
>

<aside>
💡 서버에서만 호출
(클라이언트는 통신 시 포트번호가 자동으로 부여되기 때문)

</aside>

### bind() 파라미터 → sockfd

OS에게 어떤 소켓에 아이피 주소와 포트번호를 부여할지 알려주기 위해
파라미터에 소켓의 파일 디스크립터를 포함 (서버의 Ip/포트번호 부여)

### bind() 파라미터 → sockaddr, socklen_t

- `sockaddr`: 소켓에 바인딩 할 아이피 주소, 포트번호를 담은 구조체
- `socklen_t` ***:*** 위 구조체의 메모리 크기
51 changes: 51 additions & 0 deletions docs/chapter1/Ch01Keyword_08.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
Keyword: listen() 시스템 콜

### listen(**sockfd, backlog**);

> **서버 소켓을 대기 상태로 전환하여 클라이언트의 연결을 수락할 수 있도록 하는 시스템 콜**
1. 파라미터로 받은 파일 디스크립터(sockfd), 즉 이 서버 소켓을 클라이언트의 요청을 받아들일 소켓으로 설정함
2. 이 서버 소켓이 클라이언트 소켓을 최대로 받아주는 크기를 backlog로 설정

**→ 연결지향인 TCP에서만 사용**
리턴 값: X (int 0)
>

<aside>
💡 서버에서만 호출

</aside>

### listen() 파라미터 → sockfd

- 파라미터로 받은 **서버 소켓**(파일 디스크립터(`sockfd`) 에 해당)

→ socket() 함수를 통해 생성된 서버 소켓의 식별자

→ 이는 클라이언트의 연결 요청을 받아들일 소켓

- 파라미터로 받은 파일 디스크립터(sockfd) 에 해당하는 이 소켓에게

**클라이언트의 연결 요청을 받아들이도록 부여함**


### listen() 파라미터 → backlog

- **파라미터로 받은 backlog 크기만큼,
서버 소켓이 클라이언트의 연결 요청을 대기하는 backlog queue를 만듦

backlog → 소켓을 최대로 받아주는 큐의 최대 크기**
- 이 backlog queue에는 연결 요청을 대기하는 클라이언트 소켓이 들어감
- 큐에서 대기 중인 연결 요청 호출 → 이건 accpet() 시스템 콜이 처리

1. 서버 측의 소켓은 `listen()`이후 대기 상태에서 클라이언트의 연결 요청을 받아주기 위해
**backlog queue**를 가진 채로 기다리게 됨
2. 실제로는 서버에 셀 수 없이 많은 클라이언트가 요청을 보내게 되고 이 요청들은 모두 backlog queue에 저장됨

**** client가 클라이언트 소켓을 통해 처음으로 서버에 요청을 하여
백로그 큐에 들어갈 때 *syn* 요청을 보내게 된다**

<aside>
💡 이 시스템 콜은 클라이언트 소켓이 아니라 서버 소켓에 적용되는 설정이며,
이 설정을 통해 서버가 너무 많은 연결 요청을 받지 않도록 함

</aside>
49 changes: 49 additions & 0 deletions docs/chapter1/Ch01Keyword_09.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
Keyword: accpet() 시스템 콜
### **accpet(sockfd, sockaddr , socklen_t**);

> **backlog queue에서 *syn(연결 요청)*을 보내와 대기 중인 요청을
선입선출(자료구조 큐)로 하나씩 연결에 대한 수립(Established)을 해주는 콜**

****리턴 값: 새로운 파일 디스크립터(소켓)
>

<aside>
💡 서버에서만 호출

</aside>

### socket() 파라미터 → sockfd

- listen()에서 클라이언트 소켓의 연결을 받는 **서버 소켓**

### socket() 파라미터 → sockaddr

- 클라이언트의 아이피 주소, 포트번호를 받는 구조체
- 백로그 큐에서 가장 앞에 있는 연결요청 구조체에서 알아내서 가져온다

### socket() 파라미터 → socklen_t

- `sockaddr` 구조체의 메모리 크기

### 이 과정에서, `TCP 3-way handshake` 진행

이때 클라이언트와 서버간에 서로 신뢰성있는 통신을 하기 위해 서버 프로세스와 클라이언트 프로세스 간의 연결 확인이 이루어진다 → `TCP 3-way handshake`

<aside>
💡 1. listen() 시스템 콜 이후 대기 상태인 서버의 소켓에 클라이언트가 연결 요청 “**SYN**”을 보내 두었었음

( SYN: 첫 번째 handshake 과정 )

1. 이후 나머지 두 과정은 accept() 시스템 콜 이후 진행

최종적으로 **Established 상태**를 수립 하고 본격적인 데이터의 송/수신이 이루어진다.

</aside>

### accept() 이후 과정

- 당연하게도, accept 시스템 콜 이후 이후 나머지 두 개의 3-handshake 과정도 운영체제가 제공하는 system call을 통해 이루어진다!
- 최종적으로,
**SYN(클라이언트) → accept()(서버) → 멀티 프로세스/멀티 쓰레드 → SYN, ACK(서버) → ACK(클라이언트) (잔여 handshake)** 의 과정을 거치게 됨

여기서 멀티 프로세스 과정은 무엇인가?
34 changes: 34 additions & 0 deletions docs/chapter1/Ch01Keyword_10.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
Keyword: 멀티 프로세스

<aside>
💡 **왜 accept() 시스템 콜의 리턴 값은 새로운 소켓의 파일 디스크립터인가?**
accept 시스템 콜 이후 곧바로 잔여 3-way handshake와 데이터 송수신이 이루어지는 것이 아니라, 서버의 성능을 위해 또 하나의 테크닉이 들어가게 된다.

</aside>

- 하나의 프로세스인 서버가 클라이언트의 수많은 요청을 받는 상황에서,

백로그 큐의 가장 앞에 있던 클라이언트의 요청을 받고 응답까지 다 주고 다시 다음 요청을 받아준다면, **엄청난 병목이 생길 것**

- 따라서 서버는 **연결 요청을 받는 부분 따로, 이후 응답까지 주는 부분을 따로** 나누게 됨

<aside>
📌 accept 시스템 콜의 응답을 받았다
(accept 과정이므로, 잔여 handshake 이전)
= SYN 요청을 보낸 클라이언트가 적어도 하나 있어서 백로그 큐에 있었고 해당 클라이언트의 요청에 대한 이후 응답을 위해 **새로운 소켓을 만들었다.**

</aside>

- 이 새로운 소켓은 fork() 시스템 콜을 통해 생긴 accpet() 시스템 콜의 자식 프로세스


| 부모 프로세스 | 부모 프로세스는 연결 요청을 받아주고 자식 프로세스에게
나머지 일을 맡기고 다시 새로운 연결요청을 받음 |
| --- | --- |
| 자식 프로세스 | 부모 프로세스가 새로 만들어준 소켓을 이어받아
이후 남은 잔여 3-way handshake를 수행 후 데이터 통신을 수행 |

<aside>
💡 이때 자식 프로세스는 새로운 연결요청을 받지 않고 그저 응답을 준 후 exit(0)를 통해 종료된다!

</aside>
Loading

0 comments on commit cc31478

Please sign in to comment.