Computer Structure
컴퓨터 구조를 알아야 하는 이유
컴퓨터 구조를 이해하고 있다면 문제 상황에 빠르게 진단할 수 있고, 문제 해결의 실마리를 다양하게 찾을 수 있다.
즉, 컴퓨터 구조를 이해하면 문제 해결 능력이 향상되고, 성능/용량/비용을 고려하며 개발할 수 있다.
컴퓨터가 이해하는 정보
컴퓨터 구조는 ‘컴퓨터가 이해하는 정보’와 ‘컴퓨터의 네 가지 핵심 부품’ 2가지로 나눌 수 있다.
컴퓨터는 0과 1로 표현된 정보만을 이해한다. 데이터와 명령어는 0과 1로 표현되는 정보이다.
데이터는 컴퓨터가 이해하는 숫자, 문자, 이미지, 동영상 등 정적인 정보를 말한다.
컴퓨터와 주고 받는 정보나 컴퓨터에 저장된 정보를 가리킬 때 데이터라 통칭하기도 한다.
컴퓨터는 명령어를 처리하는 기계이다.
명령어는 데이터를 움직이고 컴퓨터를 작동시키는 정보이다.
명령어 없이는 데이터는 아무것도 할 수 없는 정보 덩어리 이다.
즉, 명령어는 컴퓨터를 작동시키는 정보이고, 데이터는 명령어를 위해 존재하는 일종의 재료이다.
컴퓨터 프로그램을 명령어들의 모음으로 정의할 수도 있다.
컴퓨터의 4가지 핵심 부품
컴퓨터를 이루는 핵심 부품은 중앙처리장치(CPU), 주기억장치(메모리), 보조기억장치, 입출력 장치 이다.
메모리
컴퓨터가 이해하는 정보는 명령어와 데이터이다. 메모리는 현재 실행되는 프로그램의 명령어와 데이터를 저장하는 부품이다. 즉, 프로그램이 실행되려면 반드시 메모리에 저장되어 있어야 한다.
컴퓨터가 빠르게 작동하기 위해서는 메모리 속 명령어와 데이터가 정돈되어 있어야 한다. 메모리 값에 빠르고 효율적으로 접근하기 위해 주소 라는 개념이 사용된다.
명령어와 데이터는 모두 0과 1로 표현된다.
- 프로그램이 실행되기 위해서는 반드시 메모리에 저장되어 있어야 한다.
- 메모리는 현재 실행되는 프로그램의 명령어와 데이터를 저장한다.
- 메모리에 저장된 값의 위치는 주소로 알 수 있다.
CPU
CPU는 메모리에 저장된 명령어를 읽어 들이고, 읽어들인 명령어를 해석하고, 실행하는 부품이다.
CPU의 내부 구성 요소는 다음과 같다.
- 산술논리연산장치(ALU : Arithmetic Logic Unit)
- 레지스터(Register)
- 제어장치(CU : Control Unit)
ALU는 쉽게 말하면 계산기이다. 컴퓨터 내부에서 수행되는 대부분의 계산은 ALU에서 수행한다.
레지스터는 CPU 내부의 임시 저장 장치이다. 프로그램을 실행하는 데 필요한 값들을 임시로 저장한다. CPU 안에는 여러 개의 레지스터가 존재하고, 각각 다른 이름과 역할을 가지고 있다.
제어장치는 제어 신호라는 전기 신호를 내보내고 명령어를 해석하는 장치이다. 제어 신호는 컴퓨터 부품들을 관리하고 작동시키기 위한 일종의 전기 신호이다.
- CPU가 메모리에 저장된 값을 읽고 싶을 땐 메모리를 향해 메모리 읽기 라는 제어 신호를 보낸다.
- CPU가 메모리에 어떤 값을 저장하고 싶을 땐 메모리를 향해 메모리 쓰기 라는 제어 신호를 보낸다.
CPU 내부 동작
- 제어장치에서 메모리에 저장된 명령어를 읽기 위해 ‘메모리 읽기’ 제어 신호를 보낸다.
- 메모리는명령어를 CPU에게 건내주고, 이 명령어는 레지스터에 저장된다.
- 제어장치는 읽어 들인 명령어를 해석한 뒤 추가적인 데이터가 필요하다 판단해, 메모리에 ‘메모리 읽기’ 제어 신호를 보낸다.
- 메모리는 저장된 데이터를 CPU에 건네주고, 이 데이터들은 서로 다른 레지스터에 저장된다.
- ALU는 읽어 들인 데이터로 연산을 수행한다.
- 계산의 결괏값은 레지스터에 저장된다.
- 다음 명령어를 읽어 들이기 위해 메모리에 ‘메모리 읽기’ 제어 신호를 보낸다.
- 메모리는 명령어를 CPU에 건네주고, 레지스터에 저장된다.
- 제어장치는 명령어를 해석한 뒤 메모리에 계산 결과를 저장해야 한다고 판단한다.
- 제어장치는 계산 결과를 저장하기 위해 ‘메모리 쓰기’ 제어 신호와 함께 계산 결과를 보낸다.
- 메모리가 계산 결과를 저장한다.
- CPU는 메모리에 저장된 값을 읽어 들이고, 해석하고, 실행하는 장치이다.
- CPU 내부에는 ALU, 레지스터, 제어장치가 있다.
- ALU는 계산하는 장치, 레지스터는 임시 저장 장치, 제어장치는 제어 신호를 발생시키고 명령어를 해석하는 장치이다.
보조기억 장치
메모리는 가격이 비싸 저장 용량이 적고, 전원이 꺼지면 저장된 내용을 잃는다는 휘발성을 가지고 있다.
보조기억장치는 메모리보다 크기가 크고, 전원이 꺼져도 저장한 내용을 잃지 않는 비휘발성을 가지고 있다.
하드디스크, SSD, USB, CD 같은 저장 장치가 보조기억장치 이다.
메모리가 현재 실행되는 프로그램을 저장한다면, 보조기억장치는 보관할 프로그램을 저장한다.
입출력장치
입출력장치는 마이크, 스피커, 프린터, 마우스, 키보드 처럼 컴퓨터 외부에 연결되어 컴퓨터 내부와 정보를 교환하는 장치를 의미한다.
메인보드와 시스템 버스
컴퓨터의 핵심 부품들은 모두 메인보드에 연결된다.
메인보드에 연결된 부품들은 서로 정보를 주고받을 수 있는데, 이는 메인보드 내부에 버스라는 통로가 있기 때문이다.
컴퓨터 내부에는 다양한 종류의 통로, 즉 버스가 존재한다.
여러 버스 중 컴퓨터의 네 가지 핵심 부품을 연결하는 중요한 버스는 시스템 버스 이다.
시스템 버스는 주소 버스, 데이터 버스, 제어 버스로 구성되어 있다.
주소 버스는 주소를 주고 받는 통로, 데이터 버스는 명령어와 데이터를 주고받는 통로, 제어 버스는 제어 신호를 주고받는 통로이다.
- CPU의 제어장치는 제어 버스를 통해 제어 신호를 내보낸다.
CPU가 메모리를 읽을 때 제어 신호만 내보내는 것이 아니다.
- 제어 버스로 ‘메모리 읽기’ 제어 신호를 내보낸다.
- 주소 버스로 읽고자 하는 주소를 내보낸다.
- 메모리는 데이터 버스로 CPU가 요청한 주소에 있는 내용을 보낸다.
값을 저장할 때도 다음과 같다.
- 데이터 버스를 통해 메모리에 저장할 값을 보낸다.
- 주소 버스를 통해 저장할 주소를 보낸다.
- 제어 버스를 통해 ‘메모리 쓰기’ 제어 신호를 보낸다.
컴퓨터의 4가지 핵심 부품은 메인보드에 연결되어 시스템 버스를 통해 서로 정보 또는 데이터를 주고 받는다.
Operating System
운영체제란?
모든 프로그램은 하드웨어를 필요로 한다.
프로그램에 실행에 필요한 요소들을 가리켜 시스템 자원 이라 한다.
컴퓨터 부품들온 모두 자원이라 할 수 있다.
즉, 모든 프로그램은 실행되기 위해 자원이 필요하다.
실행할 프로그램에 필요한 자원을 할당하고, 프로그램이 올바르게 실행되도록 돕는 특별한 프로그램이 운영체제이다.
운영체제도 프로그램이기 때문에, 메모리에 적재되어야 한다. 하지만 특별한 프로그램이기 때문에 항상 컴퓨터가 부팅될 때 메모리 내 커널 영역이라는 공간에 따로 적재되어 실행된다.
커널 영역을 제외한 영역, 즉 사용자가 이용하는 응용 프로그램이 적재되는 영역을 사용자 영역이라 한다.
운영체제는 커널 영역에 적재되어 사용자 영역에 적재된 프로그램들에 자원을 할당하고 실행되도록 돕는다.
프로그램을 실행할 때, 메모리 주소가 겹치지 않도록 적당한 공간에 적재해준건 운영체제이다.
운영체제는 실행할 프로그램을 메모리에 적재하고, 더 이상 실행되지 않는 프로그램을 메모리에서 삭제하며 지속적으로 메모리 자원을 관리한다.
또한, 프로그램이 실행되려면 반드시 CPU가 필요하다. 어떤 프로그램부터 CPU를 사용할지, 얼마나 사용할지 또한 운영체제가 해결한다.
운영체제는 최대한 공정하게 여러 프로그램에 CPU 자원을 할당한다.
운영체제는 운영 프로그램과 하드웨어 사이에서 응용 프로그램에 필요한 자원을 할당하고 응용 프로그램이 올바르게 실행되도록 관리한다.
운영체제는 관리할 자원별로 기능이 나누어져 있다.
- 운영체제는 실행할 프로그램에 필요한 자원을 할당하고, 프로그램이 올바르게 실행되도록 돕는 프로그램이다.
운영체제를 알아야 하는 이유
운영체제가 없다면, 하드웨어를 조작하는 코드를 개발자가 모두 직접 작성해야 한다.
운영체제가 하드웨어를 조작하고 관리하는 기능들을 제공하기 때문에 개발자는 하드웨어를 조작하는 코드를 직접 작성할 필요 없이 운영체제의 도움을 받아 간편하게 개발할 수 있다.
운영체제를 이해하면 문제 해결에 도움을 줄 수 있다.
대표적으로 오류 메시지로 어떤 문제인지 진단할 수 있고 해결할 수 있다.
커널
운영체제는 프로그램 중 규모가 큰 프로그램 중 하나이다.
리눅스를 구성하는 소스 코드만 천만 줄이 넘는다.
운영체제의 핵심 서비스를 담당하는 부분을 커널이라 한다.
운영체제가 제공하는 서비스 중 커널에 포함하지 않는 서비스는 사용자 인터페이스가 있다.
사용자 인터페이스는 사용자가 컴퓨터와 상호 작용할 수 있는 통로다.
운영체제가 제공하는 사용자 인터페이스 종류는 그래픽 유저 인터페이스(GUI), 커맨드 라인 인터페이스(CLI)가 있다.
- GUI : 그래픽을 기반으로 컴퓨터와 상호작용 할 수 있는 인터페이스
- CLI : 명령어를 기반으로 컴퓨터와 상호작용 할 수 있는 인터페이스
이중 모드와 시스템 호출
운영체제는 사용자가 실행하는 응용 프로그램이 하드웨어 자원에 직접 접근하는 것을 방지하여 자원을 보호한다.
응용 프로그램이 CPU, 메모리, 하드 디스크 등에 마음대로 접근하고 조작할 수 있다면 자원이 무질서하게 관리되고, 컴퓨터 전체에 큰 악영향을 끼친다.
운영체제는 응용 프로그램들이 자원에 접근하려 할 때 오직 자신을 통해서만 접근하도록 하여 자원을 보호한다.
이러한 역할은 이중 모드로 구현된다.
이중 모드란 CPU가 명령어를 실행하는 모드를 크게 사용자 모드와 커널 모드로 구분하는 방식이다. CPU는 명령어를 사용자 모드로써 실행할 수 있고, 커널 모드로써 실행할 수 있다.
사용자 모드는 운영체제 서비스를 제공받을 수 없는 실행 모드이다. 즉, 커널 영역의 코드를 실행할 수 없는 모드이다.
일반적인 응용 프로그램은 기본적으로 사용자 모드로 동작한다.
커널 모드는 운영체제 서비스를 제공받을 수 있는 실행 모드이다. 즉 커널 영역의 코드를 실행할 수 있는 모드이다.
CPU가 커널 모드로 명령어를 실행하면 자원에 접근하는 명령어를 비롯한 모든 명령어를 실행할 수 있다.
운영체제는 커널 모드로 실행되기 때문에 자원에 접근할 수 있다.
사용자 모드로 실행되는 프로그램이 자원에 접근하는 운영체제 서비스를 제공받으려면 운영체제에 요청을 보내 커널 모드로 전환되어야 한다.
운영체제 서비스를 제공받기 위한 요청을 시스템 콜이라 한다.
즉, 사용자 모드로 실행되는 프로그램은 시스템 콜을 통해 커널 모드로 전환하여 운영체제 서비스를 제공받을 수 있다.
시스템 콜은 소프트웨어 인터럽트이다.
시스템 콜을 발생시키는 명령어가 실행되면 CPU는 지금까지의 작업을 백업하고, 커널 영역 내에 시스템 호출을 수행하는 코드(인터럽트 서비스 루틴)를 실행한 뒤 다시 기존에 실행하던 응용 프로그램으로 복귀하여 실행을 계속해 나갑니다.
시스템 호출 작동 예
- 하드디스크는 데이터를 저장하는 시스템 콜을 발생시켜 커널 모드로 전환
- 운영체제 내의 ‘하드 디스크에 데이터를 저장하는 코드’ 실행
- 하드디스크에 접근이 끝나면, 사용자 모드로 복귀하여 실행을 계속함
프로세스 관리
실행중인 프로그램 프로세스라고 한다.
일반적으로 CPU는 한 번에 하나의 프로세스만 실행할 수 있기에, CPU는 프로세스들을 번갈아가면서 실행한다. 즉, CPU는 프로세스를 매우 빠르게 전환하며 실행한다.
각 프로세스는 상태도, 사용하고자 하는 자원도 다양하다.
여러 프로세스가 동시에 실행되는 환경에서는 ‘프로세스 동기화’가 필수적이고, ‘교착 상태’를 해결해야 한다.
자원 접근 및 할당
모든 프로세스는 실행을 위해 자원을 필요로 한다.
운영체제는 프로세스들이 사용할 자원에 접근하고 조작함으로써 프로세스에 필요한 자원을 할당해준다.
운영체제는 하드웨어를 다음과 같이 관리한다.
CPU
메모리에는 여러 프로세스가 적재되고, 하나의 CPU는 한 번에 하나의 프로세스만 실행 할 수 있다.
그래서 하나의 프로세스가 CPU를 이용하고 있다면, 다른 프로세스는 기다려야 한다.
운영체제는 공정하게 프로세스에게 CPU를 할당하기 위해 어떤 프로세스부터 CPU를 이용하게 할지, 얼마나 이용하게 할지를 결정하는데 이를 CPU 스케줄링이라 한다.
메모리
메모리에 적재된 프로세스들은 크기도, 적재되는 주소도 가지각색이다. 같은 프로세스여도 실행할 때마다 적재 주소는 달라질 수 있다.
즉, 운영체제는 새로운 프로세스가 적재될 때마다 어느 주소에 적재되어야 할지를 결정해야 한다.
메모리가 이미 꽉차, 적재할 공간이 없는 경우도 있고, 메모리 공간이 남았는 데도 불구하고 프로세스를 적재하지 못하는 상황도 발생한다.
이를 위해 운영체제가 프로세스에게 어떻게 메모리를 할당할지 결정한다.
입출력장치
인터럽트 서비스 루틴은 운영체제가 제공하는 기능으로 커널 영역에 있다.
입출력장치가 발생시키는 하드웨어 인터럽트도 마찬가지이다.
입출력 장치가 CPU에 하드웨어 인터럽트 요청 신호를 보내면 CPU는 하던 일을 잠시 백업한 뒤 커널 영역에 있는 인터럽트 서비스 루틴을 실행한다.
즉, 운영체제는 인터럽트 서비스 루틴을 제공해 입출력 작업을 수행한다.
파일 시스템 관리
컴퓨터를 사용할 때는 여러 파일을 열고, 생성하고, 삭제한다. 그리고 파일들을 묶어 디렉토리로 관리한다.
파일 시스템도 운영체제가 지원하는 핵심 서비스이다.
정리
운영체제의 핵심 서비스를 제공하는 부분은 커널이고, 사용자 프로세스가 커널에 서비스를 제공받기 위해서는 시스템 콜을 통해 사용자 모드에서 커널모드로 전환해야 한다.
그리고 커널의 대표적인 서비스로 프로세스 관리, 자원 접근 및 할당, 파일 시스템 관리가 있다.
가상 머신
가상 머신은 소프트웨어적으로 만들어낸 가상 컴퓨터이다.
가상 머신을 통해 새로운 운영체제를 설치하고 실행할 수 있다.
가상 머신 또한 응용 프로그램이라 사용자 모드로 동작한다. 가상 머신상에 설치된 운영체제 또한 사용자 모드로 작동한다. 가상 머신에 설치된 응용 프로그램이 운영체제 서비스를 제공받기 위해서넌 커널 모드로 전환되어야 하는데, 가상 머신에 설치된 운영체제도 사용자 모드로 작동하면 운영체제 서비스를 제공받기 어렵다.
가상화를 지원하는 CPU 커널 모드와 사용자 모드 이외에 가상 머신을 위한 하이퍼 바이저 모드를 따로 둔다. 즉, 가상 머신 상에서 작동하는 응용 프로그램들은 하이퍼바이저 모드로 운영체제 서비스를 받을 수 있다.
시스템 콜의 종류
프로세스 관리
- fork() : 새 자식 프로세스 생성
- execve() : 프로세스 실행(메모리 공간을 새로운 프로그램의 내용으로 덮어씌움)
- exit() : 프로세스 종료
- waitpid() : 자식 프로세스가 종료될 때 까지 대기
파일 관리
- open() : 파일 열기
- close() : 파일 닫기
- read() : 파일 읽기
- write() : 파일 쓰기
- stat() : 파일 정보 획득
디렉터리 관리
- chdir() : 작업 디렉터리 변경
- mkdir() : 디렉터리 생성
- rmdir() : 비어 있는 디렉터리 삭제
파일 시스템 관리
- mount() : 파일 시스템 마운트
- unmount() : 파일 시스템 마운트 해제
Java
예외
자바에서는 예외적인 일이 발생하면 예외를 던진다.
null 인 객체의 메서드를 호출하던가, 5 크기의 배열 중 6번째 값을 읽던가 등 여러가지 예외 상황이 있다.
예외 처리하는 방법은 다양한 방법이 있다.
try-catch
예외가 발생하지 않도록 개발하는 것이 우선이지만, 완벽하게 처리하기 어렵다.
if문 처럼 try-catch를 통해 예외처리를 할 수 있다.
try{
//예외가 발생할 가능성이 있는 코드
}catch(Exception e){
//예외가 발생하면 어떻게 처리할껀지?
}
Exception은 모든 예외의 최고 조상이므로, 모든 예외를 잡을 수 있다.
Exception이 아닌 세부적으로 예외마다 예외처리를 설정할 수도 있다.
- try-catch에서 예외가 발생하지 않을 경우
- try 내에 있는 모든 문장이 실행되고, try-catch 문장 이후의 내용이 실행된다.
- try-catch에서 예외가 발생하는 경우
- try 내에서 예외가 발생한 이후의 문장들은 실행되지 않는다.
- catch 내에 있는 문장은 반드시 실행되고, try-catch 문장 이후의 내용이 실행된다.
또한 catch 는 여러개 쓸 수 있으며, 세부적으로 처리가 가능하다.
catch 순서에 따라 처리되며, 만약 예외가 겹치면 컴파일 에러가 발생한다.
즉, Exception과 하위 Exception을 같이 처리하면, 결국 Exception 에서 모든 예외를 잡기 때문에 하위 Exception은 예외 처리 할 필요가 없다. 순서를 바꾸면 정상적으로 컴파일이 된다.
finally
try-catch 구문에 finally 구문을 추가하면, 어떠한 경우에도 반드시 실행한다.
try{
//예외가 발생할 가능성이 있는 코드
}catch(Exception e){
//예외가 발생하면 어떻게 처리할껀지?
}finally{
//반드시 실행 해야 되는 코드, 자원 닫기 등
}
예외의 종류
예외는 세 가지 종류가 있다.
- checked exception
- error
- runtime excetpion or unchecked exeception
error와 runtime exeption 을 제외하고는 모두 check exception 이다.
error
에러는 자바 프로그램 밖에서 발생한 예외를 말한다.
Exception 클래스는 에러가 아니다. 오류가 Error로 끝나면 에러이고, Exception으로 끝나야 예외이다.
Error와 Exception으로 끝나는 오류의 가장 큰 차이는 프로그램 안에서 발생했는지, 밖에서 발생했는지 여부이다.
더 큰 차이는 프로그램이 멈추냐, 계속 실행할 수 있느냐의 차이다.
Error는 프로세스에 영향을 주고, Exception은 스레드에만 영향을 준다.
runtime exception
런타임 예외는 예외가 발생할 것을 미리 감지하지 못했을 때 발생한다.
런타임 예외에 해당하는 모든 에러는 RuntimeException을 상위 클래스로 가진다.
예외처리를 하지 않아도 컴파일 오류가 발생하지 않는다.
그래서 unchecked exception이라 한다.
checked exception
체크드 익셉션은 위의 에러와 예외를 제외한 모든 예외이다.
반드시 예외 처리를 해야지 컴파일 오류가 발생하지 않는다.
그래서 checked exception 이라 한다.
java.lang.Throwable
Exception과 Error의 공통 부모 클래스는 Object이며, Throwable 클래스이다.
즉, Throwable 클래스를 상속받고, 처리를 Throwable로 처리해도 무관하다.
생성자 종류
- Throwable()
- Throwable(String message)
- Throwable(String message, Throwable cause)
- Throwable(Throwable cause)
메서드 종류
- getMessage()
- 예외 메세지를 String 형태로 제공받는다.
- toString()
- 예외 메시지를 String 형태로 제공받지만, 더 자세하게 예외 클래스 이름도 같이 제공한다.
- printStackTrace()
- 가장 첫줄에는 예외 메시지를 출력하고, 두번째 줄부터는 예외가 발생하게 된 메서드들의 호출 관계(스택 트레이스)를 출력한다.
throw
throw 는 에외를 발생시킨다.
주로 조건에 따라 처리한다.
throw new Exception("오류내용");
만약 예외처리가 되어 있지 않다면, 발생된 예외를 메서드 밖으로 던지는 방법도 있다.
메서드 뒤에 throws 예외종류를 써놓으면, 해당 예외가 발생했을 시 메서드를 호출한 곳으로 예외를 던진다.
즉, 예외를 처리를 부른곳에서 위임한다.
또한 여러 종류의 예외를 던질 수 도 있다.
커스텀 예외
예외를 상속받으면 사용자가 직접 예외를 만들 수 있다.
런타임 예외를 만들시에는 RuntimeException 을 상속 받아야 예외처리가 필수적이지 않게 된다.
Network
TCP란?
TCP란 신뢰성 있는 애플리케이션 간의 데이터 전송을 위한 프로토콜
TCP를 이용하면, 애플리케이션 프로토콜에는 신뢰성을 확보하기 위한 구조를 넣어둘 필요가 없다.
TCP에 의한 전송 절차는 다음과 같다.
- TCP 커넥션 맺기
- 애플리케이션 간 데이터 송수신
- TCP 커녁션 끊기
통신이 정상적으로 이루어질 수 있는지 확인한다. 확인 프로세스를 3 웨이 핸드셰이크 라고 한다.
애플리케이션이 다루는 데이터를 TCP로 송수신 하기 위해선, 데이터에 애플리케이션 프로토콜 헤더와 TCP 헤더를 추가할 필요가 있다. 이를 TCP 세그먼트라고도 한다.
애플리케이션의 데이터 크기가 크면 분할하여 복수의 TCP 세그먼트로 전송한다. 어떻게 분할했는지는 TCP 헤더에 기술되고, 목적지에 차례대로 원본 데이터로 조립한다.
데이터가 도착하면 받았다고 확인해 준다. 데이터 수신 확인을 ACK 라고 한다.
만약 데이터가 제대로 전송이 안되었다면 재전송을 한다.
네트워크가 혼잡해지면, 데이터 전송 속도를 제한한다. 이런 전송 구조를 플로우 제어라한다.
마지막으로 애플리케이션의 데이터 전송이 모두 끝나면, TCP 커넥션을 끊는다.
TCP 헤더 형식
TCP로 전송하고 싶은 데이터에 TCP 헤더를 추가하면 TCP 세그먼트가 된다.
TCP 헤더 형식은 다음과 같다.
- 출발지 포트 번호(16)
- 목적지 포트 번호(16)
- 시퀀스 번호(32)
- ACK 번호(32)
- 데이터 세그먼트(4)
- 예약(6)
- 플래그(6)
- 윈도우 사이즈(16)
- 체크섬(16)
- 에이전트 포인터(16)
포트 번호로 적절한 애플리케이션 프로토콜에 데이터를 배분할 수 있다.
신뢰성 있는 데이터 전송을 위해서 시퀀스 번호와 ACK 번호가 있다.
시퀀스 번호는 TCP로 전송하는 데이터 순서를 나타낸다. 데이터가 분할되어 있을 때는 시퀀스 번호로 어떻게 데이터를 분할 했는지 알 수 있다. ACK 번호는 데이터를 바르게 수신했음을 확인하기 위해 이용할 수 있다.
데이터 분할 구조
TCP에는 데이터를 분할하는 기능도 있다.
TCP 에서 애플리케이션의 데이터를 분할하는 단위는 MSS(Maximum Segment Size)라고 부른다.
MSS를 넘는 크기의 데이터는 MSS 단위로 나누어 송신한다.
MSS의 표준 크기는 1460 바이트이다.
웹 애플리케이션으로 데이터를 송신하는 경우 다음과 같다.,
HTTP 헤더 + 애플리케이션 데이터 = MSS (1460 바이트)만큼 데이터를 각각 분할한다.
각 분할된 데이터에는 TCP헤더가 있고, TCP 헤더를 제외하고 1460 바이트이다.
TCP 세그먼트를 이더넷으로 내보내기 위해 IP 헤더, 이더넷 헤더로 캡슐화 한다.
이더넷의 MTU 는 1500바이트가 표준이기 때문에 TCP 헤더를 붙이면 1480바이트가된다. 그 후 IP 헤더를 붙이면 1500 바이트가 된다.
UDP
UDP는 PC나 서버 등에 도달한 데이터를 적절한 애플리케이션에 배분하는 기능만 있는 프로토콜이다. TCP 처럼 확인은 하지 않는다.
UDP로 애플리케이션 데이터 송수신을 하기 위해선 UDP 헤더를 추가한다. UDP 헤더와 애플리케이션의 데이터를 합쳐 UDP 데이터그램이라 부른다.
TCP 헤더 형식에 비해 UDP 헤더 형식은 단순하다.
UDP는 상대방의 애플리케이션의 동작 유무와 상관없이 무조건 UDP 데이터그램으로 애플리케이션의 데이터를 송신한다.
TCP 만큼 여분의 처리를 하지 않으므로, 데이터 전송 효율이 좋다.
반면에 신뢰성이 높지 않다는 단점이 있다.
UDP의 경우 보내고 싶은 UDP 데이터그램이 상대방 까지 제대로 도달했는지 알 수 가 없다.
UDP에서 크기가 큰 데이터를 분할하는 기능도 없다. 그러므로 전송해야 할 애플리케이션의 데이터 크기가 클 경우 애플리케이션에서 적절한 크기를 조절해야 한다.
IP 전화가 대표적인 예시이다.
IP 전화의 음성 데이터는 IP 전화에서 작게 쪼개고, 작게 쪼갠 음성 데이터에 UDP 헤더를 추가해서 전송한다.
음성 데이터나, 실시간 데이터 전송을 해야할 때 UDP를 이용한다.
UDP 헤더 형식
- 출발지 포트 번호(16)
- 도착지 포트 번호(16)
- 데이터그램 길이(16)
- 체크섬(16)
IP 주소 지정
TCP/IP로 통신할 때는 통신 상대방의 IP 주소를 반드시 지정해야 한다.
IP 주소가 필요하다고 해도, 애플리케이션을 이용하는 사용자가 IP 주소를 이해하긴 어렵다. 그래서 애플리케이션이 동작하는 서버는 클라이언트 PC 등의 호스트에 사용자가 이해하기 쉬운 이름인 호스트명을 붙인다.
애플리케이션을 이용하는 사용자가 의식하는 것은 웹사이트 주소인 URL과 메일 주소이다. URL과 메일 주소에는 호스트명 자체나 호스트 이름을 구하기 위한 정보가 포함된다.
사용자가 URL 등으로 애플리케이션의 주소를 지정하면, 호스트 이름에 대응하는 IP 주소를 자동으로 구하는 것이 DNS 이다.
호스트명으로부터 IP 주소를 구하는 방법을 이름해석이라고 부른다.
DNS는 가장 자주 이용되는 이름 해석 방법이다.
네트워크의 전화번호부
DNS는 전화번호부의 느낌이다. 번호를 몰라도 이름으로 전화를 걸 수 있듯이 TCP/IP 통신에 필요한 IP 주소는 DNS에 문의해서 조사한다.
DNS 서버
DNS를 이용하려면 DNS 서버가 필요하다.
DNS 서버에 미리 호스트명과 IP 주소의 대응 관계를 등록해 둔다.
DNS 서버에는 호스트명의 IP 주소의 대응 관계 뿐 아니라 그 밖에도 여러가지 정보를 등록한다.
DNS 서버에서 등록하는 정보를 리소스 레코드라고 한다.
- A : 호스트명에 대응하는 IP 주소
- AAAA : 호스트명에 대응하는 IPv6 주소
- CNAME : 호스트명에 대응하는 별명
- MX : 도메인명에 대응하는 메일 서버
- NS : 도메인명을 관리하는 DNS 서버
- PRT : IP 주소에 대응하는 호스트명
DNS의 이름 해석
DNS 서버에 필요한 정보(리소스 레코드)를 바르게 등록하는 것이 대전제이다.
DNS 서버는 루트를 정점으로 한 계층 구조로 되어 있다.
애플리케이션을 이용하는 사용자가 호스트 이름을 지정하면, 자동으로 DNS 서버에 대응하는 IP 주소를 질의한다. DNS 서버에 질의하는 기능은 윈도우 등 OS에 내정되어 있고, DNS 리졸버라 한다.
질의한 호스트명에 관한 정보가 반드시 가까운 DNS 서버에 있다고 할 수 없다. 자신이 관리하는 도메인 이외의 호스트명을 찾으려면 루트에서부터 여러번 질의를 반복해야 한다.
이처럼 이름해석을 반복해서 묻는 것을 재귀질의라고 부른다.
하지만 매번 루트에서부터 재귀질의를 하는 것은 효율적이지 않기 때문에, DNS 서버와 리졸버는 질의한 정보를 캐시에 보존한다.
통신하기 위해서는 설정이 필요
TCP/IP를 이용해 통신하기 위해선 바르게 TCP/IP 설정이 되어 있어야 한다.
설정을 자동화하는 프로토콜이 DHCP 이다.
DHCP를 이용하려면 미리 DHCP 서버를 준비하고, 할당할 IP 주소 등 TCP/IP 설정을 등록해둔다.
그리고 PC 등에서 DHCP 클라이언트가 되도록 설정한다.
DHCP 클라이언트의 호스트가 네트워크에 접속하면, DHCP 서버는 다음 메시지를 주고 받으며 자동으로 TCP/IP 설정을 한다.
- DHCP DISCOVER : DHCP 서버 있는지 확인 (클라이언트 → 서버)
- DHCP OFFER : 사용할 수 있는 TCP/IP 설정 응답 (서버 → 클라이언트)
- DHCP REQUEST : 설정 정보를 사용한다고 다시 응답 (클라이언트 → 서버)
- DHCP ACK : 서버에서 확인 응답 (서버 → 클라이언트)
DHCP의 통신은 브로드캐스트를 이용한다. 애초에 DHCP 클라이언트는 자신의 IP 주소는 물론이고 DHCP 서버의 IP 주소조차 알 수 없다. 주소를 몰라도 브로드 캐스트를 이용한다.
TCP/IP 설정 항목은 다음과 같다.
- IP주소/서브넷 마스크
- 기본 게이트웨이의 IP 주소
- DNS 서버의 IP 주소
웹사이트란?
웹사이트는 웹 서버 애플리케이션이 공개하는 다양한 웹페이지의 집합이다.
웹 사이트를 만드려면 웹서버에 웹서버 애플리케이션을 설치하고, 공개할 웹페이지를 결정해야한다.
일반적으로 HTML 파일로 만든다.
웹 사이트를 구성하는 웹페이지의 파일을 웹서버 애플리케이션에서 웹브라우저로 전송하여 표시한다.
- 웹브라우저에서 웹사이트 주소를 입력하거나 링크를 클릭하면, 웹서버 애플리케이션에 파일 전송 요청을 보낸다.
- 웹 서버 애플리케이션은 요청받은 파일을 응답으로서 돌려보낸다.
- 웹브라우저에서 수신한 파일을 표시하면, 웹사이트를 볼 수 있게 된다.
웹사이트를 볼 때, 웹 브라우저와 웹 서버 애플리케이션 사이의 웹페이지 파일 전송이 한 번으로 끝나는 것은 아니다.
필요하면 여러 번 파일 전송을 반복한다.
웹페이지 파일 전송에 이용하는 TCP/IP의 애플리케이션층 프로토콜은 HTTP 이다. 암호화하는 경우 HTTPS 를 사용한다.
애플리케이션층에서 인터넷층까지 프로토콜의 조합은 웹브라우저도 웹서버 애플리케이션도 모두 같다.
최하층인 네트워크 인터페이스층 프로토콜은 같은 것을 사용할 필요가 없다.
Linux
프로세스 생성의 목적
리눅스에서는 두 가지 목적으로 프로세스를 생성한다.
- 같은 프로그램의 처리를 여러 개의 프로세스가 나눠서 처리한다. 웹 서버 처럼 요청이 여러개 왔을 때 동시에 처리해야 하는 경우
- 전혀 다른 프로그램을 생성한다. 예를 들어 bash로 부터 각종 프로그램을 새로 생성하는 경우
위의 생성 목적에 fork()와 execve() 함수를 사용한다.(시스템 내부에서는 clone()과 execve() 시스템콜을 호출한다)
fork() 함수
목적 1 처럼 여러 개로 나눠서 처리해야 되는 경우 fork() 함수만을 사용한다.
fork() 함수를 실행하면 실행한 프로세스와 함께 새로운 프로세스가 1개 생성된다.
생성 전의 프로세스를 부모 프로세스, 새롭게 생성된 프로세스를 자식 프로세스라고 부른다.
프로세스를 생성하는 순서는 다음과 같다.
- 자식 프로세스용 메모리 영역을 작성하고 거기에 부모 프로세스의 메모리를 복사한다.
- fork() 함수의 리턴값이 각기 다른 것을 이용하여 부모 프로세스와 자식 프로세스가 서로 다른 코드를 실행하도록 분기한다.
#include <unistd.h>
#incldue <stdio.h>
#include <stdlib.h>
#include <err.h>
static void child(){
printf("I', child! my pid is %d.\\n", getpid());
exit(EXIT_SUCCESS);
}
static void parent(pid_t pid_c){
printf("I'm parent! my pid is $d and the pid of my child is %d.\\n", getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void){
pid_t ret;
ret = fork();
if(ret == -1)
err(EXIT_FAILURE, "fork() failed");
if(ret == 0)
child();
else
parent(ret);
err(EXIT_FAILURE, "shouldn't reach here");
}
root@cce021236ef5:/com# cc -o fork fork.c
root@cce021236ef5:/com# ./fork
I'm parent! my pid is 28 and the pid of my child is 29.
I' m child! my pid is 29.
프로세스 ID가 28인 프로세스가 분기 실행되어, 부모 프로세스에게 프로세스 ID 29의 자식 프로세스가 생성되었고, fork() 함수 실행 뒤에 두 프로세스의 처리가 분기되어 실행되고 있다.
execve() 함수
전혀 다른 프로그램을 생성할 때에는 execuve() 함수를 사용한다.
커널이 각각의 프로세스를 실행하기까지의 흐름은 다음과 같다.
- 실행 파일을 읽은 다음 프로세스의 메모리 맵에 필요한 정보를 읽어 들인다.
- 현재 프로세스의 메모리를 새로운 프로세스의 데이터로 덮어쓴다.
- 새로운 프로세스의 첫 번째 명령부터 실행한다.
즉, 전혀 다른 프로그램을 생성하는 경우 프로세스의 수가 증가하는 것이 아닌, 기존의 프로세스를 별도의 프로세스로 변경하는 방식으로 수행된다.
실행 파일을 읽고 프로세스의 메모리 맵에 필요한 정보를 읽어 들인다.
실행 파일은 프로세스의 실행 중에 사용하는 코드와 데이터 이외에도 다음 정보가 필요하다.
- 코드를 포함한 데이터 영역의 오프셋, 사이즈, 메모리 맵 시작 주소
- 코드 외의 변수 등에서 데이터 영역에 대한 정보(오프셋, 사이즈, 메모리 맵 시작 주소)
- 최초로 실행할 명령의 메모리 주소(엔트리 포인트)
코드 영역과 데이터 영역에서 메모리 맵 시작 주소가 필요한 이유는 CPU에서 실행되는 기계언어 명령언 고급 언어로 쓰인 소스코드와 다르게 특정 메모리를 지정해야 하기 때문이다.
ELF
리눅스의 실행 파일은 실제로는 위에서 설명한 것과 같은 단순한 것이 아니라 ELF(Executable Inkable Format) 이라는 형식을 사용한다.
root@cce021236ef5:/com# readelf -h /bin/sleep
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: DYN (Position-Independent Executable file)
Machine: AArch64
Version: 0x1
Entry point address: 0x1c80
Start of program headers: 64 (bytes into file)
Start of section headers: 25192 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 56 (bytes)
Number of program headers: 9
Size of section headers: 64 (bytes)
Number of section headers: 28
Section header string table index: 27
- 0x1c80 주소가 엔트리 포인트 주소이다.
코드와 데이터 영역의 파일상의 오프셋, 사이즈, 메모리 맵 시작 주소를 얻으려면 -S 옵션을 추가한다.
root@cce021236ef5:/com# readelf -S /bin/sleep
There are 28 section headers, starting at offset 0x6268:
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .interp PROGBITS 0000000000000238 00000238
000000000000001b 0000000000000000 A 0 0 1
[ 2] .note.gnu.bu[...] NOTE 0000000000000254 00000254
0000000000000024 0000000000000000 A 0 0 4
[ 3] .note.ABI-tag NOTE 0000000000000278 00000278
0000000000000020 0000000000000000 A 0 0 4
[ 4] .gnu.hash GNU_HASH 0000000000000298 00000298
000000000000001c 0000000000000000 A 5 0 8
[ 5] .dynsym DYNSYM 00000000000002b8 000002b8
00000000000005d0 0000000000000018 A 6 3 8
[ 6] .dynstr STRTAB 0000000000000888 00000888
00000000000002c5 0000000000000000 A 0 0 1
[ 7] .gnu.version VERSYM 0000000000000b4e 00000b4e
000000000000007c 0000000000000002 A 5 0 2
[ 8] .gnu.version_r VERNEED 0000000000000bd0 00000bd0
0000000000000050 0000000000000000 A 6 2 8
[ 9] .rela.dyn RELA 0000000000000c20 00000c20
00000000000002e8 0000000000000018 A 5 0 8
[10] .rela.plt RELA 0000000000000f08 00000f08
0000000000000480 0000000000000018 AI 5 22 8
[11] .init PROGBITS 0000000000001388 00001388
0000000000000018 0000000000000000 AX 0 0 4
[12] .plt PROGBITS 00000000000013a0 000013a0
0000000000000320 0000000000000000 AX 0 0 16
[13] .text PROGBITS 00000000000016c0 000016c0
00000000000027b0 0000000000000000 AX 0 0 64
[14] .fini PROGBITS 0000000000003e70 00003e70
0000000000000014 0000000000000000 AX 0 0 4
[15] .rodata PROGBITS 0000000000003e88 00003e88
00000000000008ef 0000000000000000 A 0 0 8
[16] .eh_frame_hdr PROGBITS 0000000000004778 00004778
000000000000008c 0000000000000000 A 0 0 4
[17] .eh_frame PROGBITS 0000000000004808 00004808
0000000000000398 0000000000000000 A 0 0 8
[18] .init_array INIT_ARRAY 0000000000015b90 00005b90
0000000000000008 0000000000000008 WA 0 0 8
[19] .fini_array FINI_ARRAY 0000000000015b98 00005b98
0000000000000008 0000000000000008 WA 0 0 8
[20] .data.rel.ro PROGBITS 0000000000015ba0 00005ba0
0000000000000060 0000000000000000 WA 0 0 8
[21] .dynamic DYNAMIC 0000000000015c00 00005c00
0000000000000200 0000000000000010 WA 6 0 8
[22] .got PROGBITS 0000000000015e00 00005e00
0000000000000200 0000000000000008 WA 0 0 8
[23] .data PROGBITS 0000000000016000 00006000
00000000000000d8 0000000000000000 WA 0 0 8
[24] .bss NOBITS 00000000000160d8 000060d8
0000000000000118 0000000000000000 WA 0 0 8
[25] .gnu_debugaltlink PROGBITS 0000000000000000 000060d8
000000000000004a 0000000000000000 0 0 1
[26] .gnu_debuglink PROGBITS 0000000000000000 00006124
0000000000000034 0000000000000000 0 0 4
[27] .shstrtab STRTAB 0000000000000000 00006158
- 출력된 내용은 두 줄이 하나의 정보 세트이다.
- 수치는 모두 16진수
- 세트 중 첫 줄의 두번째 필드가 .text이면 코드 영역 정보, .data면 데이터 영역의 정보를 의미한다.
- 세트의 다음 위치를 보면 정보를 알 수 있다.
- 메모리 맵 시작 주소 : 첫 줄의 네번째 필드
- 파일상의 오프셋 : 첫줄의 다섯번째 필드
- 사이즈 : 둘째줄의 첫 번째 필드
root@cce021236ef5:/com# /bin/sleep 10000 &
[1] 32
root@cce021236ef5:/com# cat /proc/32/maps
aaaaddc90000-aaaaddc95000 r-xp 00000000 fe:01 2493927 /usr/bin/sleep -- 코드 영역
aaaaddca5000-aaaaddca6000 r--p 00005000 fe:01 2493927 /usr/bin/sleep
aaaaddca6000-aaaaddca7000 rw-p 00006000 fe:01 2493927 /usr/bin/sleep
aaab1032b000-aaab1034c000 rw-p 00000000 00:00 0 [heap]
ffff9a050000-ffff9a1d9000 r-xp 00000000 fe:01 2494290 /usr/lib/aarch64-linux-gnu/libc.so.6
ffff9a1d9000-ffff9a1e8000 ---p 00189000 fe:01 2494290 /usr/lib/aarch64-linux-gnu/libc.so.6
ffff9a1e8000-ffff9a1ec000 r--p 00188000 fe:01 2494290 /usr/lib/aarch64-linux-gnu/libc.so.6
ffff9a1ec000-ffff9a1ee000 rw-p 0018c000 fe:01 2494290 /usr/lib/aarch64-linux-gnu/libc.so.6
ffff9a1ee000-ffff9a1fa000 rw-p 00000000 00:00 0
ffff9a204000-ffff9a22f000 r-xp 00000000 fe:01 2494272 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffff9a239000-ffff9a23b000 rw-p 00000000 00:00 0
ffff9a23b000-ffff9a23d000 r--p 00000000 00:00 0 [vvar]
ffff9a23d000-ffff9a23e000 r-xp 00000000 00:00 0 [vdso]
ffff9a23e000-ffff9a240000 r--p 0002a000 fe:01 2494272 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffff9a240000-ffff9a242000 rw-p 0002c000 fe:01 2494272 /usr/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1
ffffc6ee7000-ffffc6f08000 rw-p 00000000 00:00 0 [stack]
root@cce021236ef5:/com# kill 32
전혀 다른 프로세스를 새로 생성할 때는 부모가 될 프로세스로부터 fork() 함수를 호출한 다음 자식 프로세스가 exec() 함수를 호출하는 방식 즉 fork and exec 방식을 주로 사용한다.
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <err.h>
static void child(){
char *args[] = {"/bin/echo", "hello", NULL};
printf("I'm child! my pid is %d.\\n", getpid());
fflush(stdout);
execve("/bin/echo", args, NULL);
err(EXIT_FAILURE, "exec() failed");
}
static void parent(pid_t pid_c){
printf("I'm parent! my pid is %d and the pid of my child is %d.\\n", getpid(), pid_c);
exit(EXIT_SUCCESS);
}
int main(void){
pid_t ret;
ret = fork();
if(ret == -1)
err(EXIT_FAILURE, "fork() failed");
if(ret == 0){
child();
} else {
parent(ret);
}
err(EXIT_FAILURE, "shouldn't reach here");
}
root@cce021236ef5:/com# vim fork-and-exec.c
root@cce021236ef5:/com# cc -o fork-and-exec fork-and-exec.c
root@cce021236ef5:/com# ./fork-and-exec
I'm parent! my pid is 44 and the pid of my child is 45.
I'm child! my pid is 45.
hello
종료 처리
프로그램 종료는 _exit() 함수를 사용한다. 내부에서는 exit_group() 시스템 콜을 호출한다.
이것을 이용하면 프로세스에 할당된 메모리를 전부 회수한다.
exit() 를 호출하는 일은 매우 드물고, 표준 C 라이브러리의 exit() 함수를 호출해서 종료한다.
'Back-end' 카테고리의 다른 글
JVM (0) | 2023.08.11 |
---|---|
2023.08.08 TIL (0) | 2023.08.09 |
2023.08.06 TIL (0) | 2023.08.07 |
2023.08.04 TIL (0) | 2023.08.04 |
2023.08.02 TIL (0) | 2023.08.02 |