본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.


TCP/IP 네트워크 개요

이더넷 프레임

  • 월드 와이드 웹(이하 WWW)은 거대한 IP 네트워크로, 연결된 기계들은 할당된 고유한 IP 주소(32비트 숫자)로 식별됨
    • IP 주소는 네트워크 주소와 호스트 주소로 나누어 구분할 수 있음
    • 호스트 주소는 서브넷(subnetwork, subnet)과 호스트 주소로 더 자세히 나눌 수 있으며, 네트워크를 사용하는 기관은 자신의 네트워크를 몇 구획으로 나눌 수 있음
    • 네트워크 관리자는 IP 주소를 할당할 때, IP 서브넷을 사용하여 네트워크 관리 부담을 분산시킴
    • IP 서브넷 관리자는 자신의 IP 서브넷 내에서 자유롭게 IP 주소를 할당

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • IP 주소는 숫자가 많아 외우기 어려우며, 문자열로 된 네트워크 이름을 부여해 기계들은 통신 할 수 있음
  • 네트워크 이름을 IP 주소로 변환해주는 작업은, /etc/hosts 파일에 정적으로 명시하거나 분산 네임 서버(Distributed Name Server, DNS)에 변환 요청을 보내서 처리
    • 로컬 호스트는 하나 이상의 DNS 서버의 IP 주소를 알고 있어야 하며, 이 주소들을 /etc/resolv.conf 에 기록
    • 웹 페이지를 읽을 때처럼 다른 호스트(서버)에 접속할 때, 데이터를 교환하기 위해 접속할 대상의 IP 주소를 사용
  • 데이터들은 IP 패킷(packet)에 담겨 전달되며, 각 패킷마다 출발지 호스트와 목적지 호스트의 IP 주소, 체크섬(checksum) 및 IP 헤더가 추가됨
    • 체크섬은 IP 패킷에 있는 데이터를 가지고 계산한 것으로, IP 패킷 수신자(목적지 호스트)는 전화선의 잡음 등에 의해 전달 중에 패킷이 손상되었는지 판단
    • 데이터는 다루기 쉽게 작은 패킷들로 쪼개져서 보내질 수 있고, 목적지 호스트는 패킷들을 다시 조합하여 프로세스에게 전달

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • 같은 IP 서브넷에 있는 호스트끼리는 IP 패킷을 직접 보낼 수 있지만, 그렇지 않으면 게이트웨이(gateway) 또는 라우터(router)라고 하는 특별한 호스트에 IP 패킷을 전송 (게이트웨이나 라우터는 한 IP 서브넷에서 다른 IP 서브넷으로 패킷을 전달하는 역할)
  • 각 호스트들은 정확한 목적지로 IP 패킷을 전달하기 위해 라우팅 테이블(routing table)을 작성해서 다음 도착지로 패킷을 전달
    • 라우팅 테이블에는 모든 IP 목적지에 대해 다음 도착지를 결정하는데 필요한 정보를 포함
    • 이 테이블은 동적으로 변경되는데, 네트워크를 사용하거나 네트워크 구성도가 변경되면 시간이 지나면서 바뀜
  • TCP 프로토콜은 신뢰할 수 있는 일대일 프로토콜로, 데이터를 주고 받기 위해 IP 프로토콜을 사용하며, IP 프로토콜은 TCP 외에 다른 프로토콜이 데이터를 보낼 때 사용하는 전송 계층
    • IP 패킷에 헤더가 붙는 것처럼 TCP 패킷에도 헤더가 추가됨
    • TCP 프로토콜로 통신하는 두 프로세스는 통신 과정에서 많은 서브넷, 게이트웨이, 라우터가 있더라도 하나의 가상 접속으로 연결됨 (연결 지향 프로토콜)
    • TCP 프로토콜은 데이터의 손실이나 중복이 없다는 것을 보장 (신뢰할 수 있는 프로토콜)
    • TCP 프로토콜이 IP 프로토콜을 통해 TCP 패킷을 전송할 때, TCP 패킷에 헤더까지 포함된 것이 IP 패킷의 데이터
    • 서로 통신하고 있는 호스트의 IP 계층은 IP 패킷을 주고 받는 역할 (받을 때는 헤더를 제거한 데이터를 TCP 계층으로 보냄)
    • UDP 프로토콜은 TCP 프로토콜과는 달리 신뢰할 수 없는 프로토콜로, IP 계층을 사용하여 데이터그램(datagram) 서비스를 제공하는데 패킷의 순서와 도착을 보장하지 않음
    • IP 프로토콜은 IP 패킷에 담긴 데이터를 전달할 상위 프로토콜을 결정하기 위해 모든 IP 패킷 헤더에 프로토콜 식별자를 지정하는 바이트가 존재 (e.g., TCP라면 IP 패킷 헤더에 데이터가 TCP 패킷인 것을 기록)
  • 프로그램이 TCP/IP 계층으로 통신할 때, 프로세스는 상대 IP 주소 외에 포트(port)도 명시해야 함
    • 포트 번호는 프로세스마다 유일한 것으로, 표준 네트워크 프로세스는 표준 포트 번호를 사용 (e.g., 웹서버의 경우 80번)
    • 등록된 포트 번호는 /etc/services 파일에서 확인 가능
  • 프로토콜의 계층 구조는 TCP/UDP 및 IP로 구분하지 않고, IP 프로토콜 자체도 패킷을 전달하는데 여러 장치를 사용하기 때문에 각 장치에서 각자의 프로토콜 헤더를 추가하기도 함
    • 예를 들어, 이더넷 네트워크에서 많은 호스트가 실제로 하나의 케이블에 동시에 접속할 수 있으므로, 전송되는 모든 이더넷 프레임은 연결된 모든 호스트에게 보여짐 (단, 모든 이더넷 장치는 고유한 주소를 가짐)
    • 호스트는 자기 주소로 전달되는 모든 이더넷 프레임을 받아들이지만, 같은 네트워크에 연결된 다른 호스트들은 이들을 무시
    • 이더넷 주소는 6바이트 길이로 (흔히 MAC 주소로 알려진), "08-00-2B-00-49-A4"와 같은 형식
    • 어떤 이더넷 주소는 멀티캐스트(multicast) 목적으로 예약되어 있어, 이 주소로 보내지는 이더넷 프레임들은 같은 네트워크 안에 있는 모든 호스트가 수신함
    • 이더넷 프레임은 데이터로 수많은 프로토콜들을 전송할 수 있기 때문에, 헤더에 프로토콜 식별자가 존재하며, 이더넷 계층은 정확하게 IP 패킷을 IP 계층에 전달 가능
  • 이더넷 같은 다중 접속 프로토콜을 통해 IP 패킷을 보내려면, IP 계층은 IP 호스트의 이더넷 주소를 탐색 (IP 주소는 개념적 주소이며, 이더넷 장치는 고유한 물리적 주소를 가짐)
    • IP 주소는 네트워크 관리자에 의해 지정되고 변경될 수 있으나, 네트워크 하드웨어는 각자의 물리적 주소 또는 멀티캐스트 주소에만 반응함 (즉, 이더넷 주소는 변경할 수 없음)
  • IP 주소를 이더넷 주소 같은 실제 하드웨어 주소로 변환하는 작업은, 주소 변환 프로토콜(ARP)을 사용해서 처리
    • 변환하고자 하는 IP 주소가 담긴 ARP 요청 패킷을 멀티캐스트 주소로 보내 모든 연결된 호스트에 전달
    • 그 IP 주소를 가지고 있는 호스트는 자신의 하드웨어 주소를 ARP 응답 패킷에 담아서 응답
    • ARP 요청이 불가능한 장치들은 별도로 표시하여 ARP를 시도하지 않음
  • 하드웨어 주소를 IP 주소로 변환하는 작업은, RARP를 사용해서 처리. 보통 이 기능은 게이트웨이가 사용하며, 원격 네트워크에 있는 IP 주소를 대신해서 게이트웨이가 ARP 요청에 응답

https://slidetodoc.com/chapter-8-arp-and-rarp-objectives-upon-completion/

리눅스의 TCP/IP 네트워크 계층

리눅스 네트워크 계층

  • 리눅스는 인터넷 프로토콜 주소 패밀리를 일련의 연관된 소프트웨어 계층으로 구현
  • BSD Sockets 계층 일반적인 소켓 관리 소프트웨어가 BSD 소켓만 처리
  • INET Sockets 계층 소켓 관리 소프트웨어를 지원하는데, IP 기반 프로토콜인 TCP/UDP의 통신 종점을 관리
  • TCP(Transmission Control Protocol) 연결 지향의 신뢰할 수 있는 일대일 프로토콜 (TCP 패킷들에 번호를 매겨 종점 호스트는 데이터를 수신할 때 패킷 순서 및 손실을 확인)

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • UDP(User Datagram Protocol) 비연결지향 방식의 프로토콜 (패킷이 전송 시 제대로 도착했는지 확인 불가)
  • IP 계층 인터넷 프로토콜을 구현한 계층으로, 전송하는 데이터 앞에 IP 헤더를 붙이고, 들어오는 IP 패킷의 헤더를 제거하여 TCP나 UDP로 전달
  • IP 계층 아래의 PPP 또는 이더넷 같은 네트워크 장치들이 리눅스의 모든 네트워킹을 지원하며, 네트워크 장치는 항상 물리 장치를 가리키는 것이 아니라 루프백 장치 같은 몇몇 순수 소프트웨어로 작성된 것도 존재
    • 네트워크 장치는 다른 장치들과 달리 관련 소프트웨어가 장치를 찾아 초기화해야 장치 파일로 보여짐
    • 해당 이더넷 디바이스 드라이버를 추가하여 커널을 빌드해야만 /dev/eth0 를 볼 수 있음

https://www.educative.io/edpresso/tcp-vs-udp

BSD 소켓 인터페이스

  • 다양한 형태의 네트워킹 외에 프로세스간 통신도 지원하는 일반적인 인터페이스
  • 통신하고 있는 두 프로세스는 연결 시, 데이터를 주고 받기 위한 소켓을 가지며, 파이프와 달리 소켓은 저장가능한 데이터 용량이 제한되지 않음
  • 주소 패밀리(Address Family) 소켓의 클래스, 각 클래스별로 통신에 사용하는 주소 표현법을 가짐
    • UNIX 유닉스 도메인 소켓
    • INET TCP/IP 프로토콜을 이용한 통신을 지원하는 인터넷 주소 패밀리
    • AX25 아마추어 라디오 X.25
    • IPX 노벨의 IPX 프로토콜
    • APPLETALK 애플사의 Appletalk DDP 프로토콜
    • X25 X.25 프로토콜
  • 소켓에는 접속을 지원하는 서비스의 종류를 나타내는 타입이 존재 (모든 주소 패밀리가 모든 형태의 서비스를 지원하지는 않음)
    • Stream 이 소켓은 데이터가 전송 중 분실, 오염, 또는 중복되지 않는다는 것을 보장하는 신뢰성 있는 양방향 순차 데이터 스트림
    • Datagram 이 소켓은 양방향 데이터 전송을 제공하나, 메시지 도착 유무, 순서 보장, 중복 제거, 오염 유무 등을 보장하지 않음
    • Raw 프로세스가 하부 프로토콜에 직접 접근 가능. 이더넷 장치에 이 소켓을 열어 가공되지 않은 IP 데이터 흐름을 볼 수 있음
    • Reliable Delivered Messages 데이터그램 소켓과 유사하지만, 데이터가 목적지에 도착한다는 것을 보장
    • Sequenced Packets 스트림 소켓과 유사하며, 데이터 패킷 크기가 고정됨
    • Packet 표준 BSD 소켓 타입이 아니며, 장치 수준에서 프로세스가 직접 패킷에 접근할 수 있는 확장 패킷 유형
  • 소켓을 사용하여 통신하는 프로세스는 클라이언트 서버 모델을 따르며, 서버는 서비스를 제공하고, 클라이언트는 이 서비스를 이용
    • 서버는 먼저 소켓을 생성하여 이름(소켓 주소 패밀리에 따라 다르며 대개 로컬 주소)을 bind한 후, 소켓의 이름 또는 주소를 sockaddr 구조체에 명시 (INET 소켓은 그것에 바인드된 IP 포트 번호를 가짐)
    • 서버는 바인드된 주소를 가리키는 연결 요청이 들어오는 listen
    • 클라이언트는 소켓을 생성하여 서버의 주소를 명시하여 서버측 소켓에 대해 연결 요청 (INET 소켓의 경우 포트 번호를 포함)
    • 연결 요청은 다양한 프로토콜 계층을 통해 전달되어 서버의 소켓에 도달하고, 서버는 요청을 받고 수락(accept) 또는 거절(reject) 할 수 있음
    • 요청을 받아들이면, 새로운 소켓을 생성 (listen 소켓은 연결 요청을 받아들이는데만 사용 가능)
    • 연결이 성립되면, 서버와 클라이언트는 자유롭게 데이터를 주고 받음
    • 연결이 필요없는 경우, 소켓을 종료(shutdown)하며, 이때 전송 중인 데이터 처리에 유의해야 함

https://rastating.github.io/using-socket-reuse-to-exploit-vulnserver/

  • 커널 초기화 과정에서 커널에 구현된 주소 패밀리는 BSD 소켓 인터페이스와 함께 커널에 자신을 등록
  • 프로세스가 BSD 소켓을 만들어 사용할 때, BSD 소켓과 지원하는 주소 패밀리 사이의 연관관계는 connection 자료구조와 주소 패밀리 고유의 지원 루틴 테이블을 통해 생성 (프로그램이 새 소켓을 생성할 때, 주소 패밀리 고유의 소켓 생성 루틴이 존재)
  • 커널을 설정할 때 주소 패밀리와 프로토콜을 protocols 배열에 추가. 이 배열에는 각 주소 패밀리 또는 프로토콜의 이름(e.g., INET)과 초기화 루틴이 포함되어 있음
  • 부팅 시 소켓 인터페이스를 초기화할 때, 각 프로토콜의 초기화 루틴이 호출되고, 소켓 주소 패밀리마다 일련의 프로토콜 연산 루틴을 등록 (이것은 루틴들의 집합이며, 각 루틴은 해당 주소 패밀리의 고유한 특정 연산을 수행)
  • proto_ops 구조체는 주소 패밀리 타입과 특정 주소 패밀리에 고유한 소켓 연산 루틴에 대한 포인터들의 집합으로 구성됨
  • pops 배열은 인터넷 주소 패밀리 같은 (AF_INET은 2) 주소 패밀리 식별자로 인덱싱

https://slidetodoc.com/socket-layer-coms-w-6998-spring-2010-erich/

INET 소켓 계층

  • TCP/IP 프로토콜을 포함하는 인터넷 주소 패밀리를 지원, 한 프로토콜이 다른 프로토콜의 서비스를 이용하며, 리눅스 TCP/IP 코드와 자료구조는 프로토콜들의 계층 구조를 반영
  • BSD 소켓 계층과의 인터페이스는 네트워크 초기화 중에 BSD 소켓 계층에 등록한 인터넷 주소 패밀리 소켓 함수들의 집합
    • BSD 소켓 계층에서 pops 배열에 등록된 다른 주소 패밀리와 함께 보관됨
    • BSD 소켓 계층은 등록된 INET proto_ops 구조체로부터 INET 계층의 소켓 지원 루틴을 호출하여 작업
    • 주소 패밀리에 INET을 주고 BSD 소켓 생성을 요구하면, 이는 하위 INET 소켓 생성 함수를 호출
  • BSD 소켓 계층은 각 함수를 호출할 때마다 INET 계층에 있는 BSD 소켓을 나타내는 socket 구조체를 전달하고, socket 구조체에 있는 data 포인터는 sock 구조체를 참조하는데, 이 구조체는 INET 소켓 계층에서 사용됨
  • sock 구조체의 프로토콜 함수 포인터는 생성 시 설정되는 것으로, 요구한 프로토콜에 따라 다름. 만약 TCP를 요구했다면, 프로토콜 함수 포인터는 TCP 연결을 위해 필요한 TCP 프로토콜 함수 집합을 참조

  • BSD 소켓 생성
    • 새 소켓을 만드는 시스템 콜에는 주소 패밀리 식별자, 소켓 타입, 프로토콜을 인자로 넘겨야 하며, 요구한 주소 패밀리를 사용하여 pops 배열에서 일치하는 주소 패밀리를 탐색
      • 어떤 주소 패밀리는 커널 모듈에 의해 생성되었기 때문에, kerneld 데몬이 이 모듈을 이용해서 주소 패밀리를 탐색
    • BSD 소켓을 나타내기 위해 새 socket 구조체를 할당. 이 구조체는 VFS inode 구조체의 일부이며 실제로 VFS inode를 할당하는 것과 같음 (이는 소켓이 일반 파일과 동일하게 작동하게 하며, 파일 함수들을 이용해 소켓을 열고, 닫고, 읽고, 쓸 수 있음)
    • socket 구조체는 주소 패밀리에 따라 특수한 소켓 루틴들에 대한 포인터를 포함하며, pops 배열에서 얻을 수 있는 proto_ops 구조체에 이 포인터들이 설정됨 (타입은 요구한 소켓 타입으로 설정: SOCK_STREAM 또는 SOCK_DGRAM 등)
    • proto_ops 구조체에 있는 주소를 호출하면, 주소 패밀리에 따라 다른 생성 루틴이 실행
    • 현재 프로세스의 fd 배열에 텅빈 파일 기술자가 할당되고 이는 초기화된 file 구조체를 참조하며 이 구조체의 파일 함수 포인터가 BSD 소켓 파일 함수들을 가리키도록 설정
    • 이후 소켓 파일 연산들은 BSD 소켓 인터페이스로 전달되며, 인터페이스는 차례로 주소 패밀리의 함수들을 호출함으로써 각 주소 패밀리로 작업을 전달
  •  bind: 주소와 INET BSD 소켓 바인딩
    • 각 서버는 INET BSD 소켓(접속 요청을 받는 listen 용도)을 만들어 서버의 주소와 바인드
      • 주소와 바인드된 소켓은 다른 통신을 위해서는 사용할 수 없음 (socket 상태는 TCP_CLOSE)
    • 바인드 작업은 대부분 INET 소켓 계층이 아래 계층인 TCP/UDP 프로토콜 계층으로부터 어느 정도 지원을 받아 처리
    • INET 주소 패밀리를 지원하며, 네트워크 장치에 할당된 IP 주소가 바인드된 주소
      • IP 주소는 모두 1 또는 0인 IP 브로드캐스트(broadcast) 주소일 수 있으며, 이는 모든 호스트에게 보내라는 의미
      • 단, 슈퍼유저 권한의 프로세스만이 아무 IP 주소에나 바인드 할 수 있음
      • 바인드된 IP 주소는 recv_addr 구조체에 있는 sock 구조체와  saddr 항목에 저장됨
    • 포트 번호는 옵션이며, 지정하지 않으면 이를 지원하는 네트워크에게 빈 포트 번호를 요청
      • 1024보다 작은 포트 번호는 슈퍼유저 권한이 없는 프로세스는 사용할 수 없음 (well-known ports)
    • 네트워크 장치는 패킷을 받으면, 이를 올바른 INET 과 BSD 소켓으로 전달하여 처리
    • TCP/UDP는 들어온 IP 메시지에 있는 주소를 조회하여 올바른 socket/sock 쌍으로 전달하기 위한 해시 테이블을 관리
      • TCP는 연결 지향 프로토콜로, UDP 패킷을 처리할 때보다 TCP 패킷을 처리할 때 더 많은 정보가 사용됨
      • UDP는 할당된 UDP 포트의 해시 테이블인 udp_hash 자료구조를 관리
      • 이는 sock 구조체의 포인터 배열로, 포트 번호에 기반하여 해시 값을 계산
    • TCP는 바인드 작업 동안에 바인드하는 sock 구조체를 해시 테이블에 추가하지 않고, 단지 요구한 포트 번호가 현재 사용중인지만 검사 (sock 구조체는 listen 작업 중에 TCP 해시 테이블에 추가됨)
  • connect: INET BSD 소켓으로 연결
    • 연결 요청을 받는 용도(listen)로 사용되지 않으면, 연결 요청을 하는 용도(connect)로 사용 가능
    • UDP는 비연결지향이므로 이런 작업이 필요 없으며, TCP만 두 프로세스 사이에 가상 연결을 생성할 때 필요함
      • UDP도 접속 BSD 소켓 함수를 지원하지만, UDP INET BSD 소켓에서의 접속 작업은 단순히 원격지의 IP 주소 및 포트 번호를 설정하는 것
      • 추가적으로 라우팅 테이블 엔트리에 대한 캐시를 설정하여, 이 BSD 소켓으로 보낸 UDP 패킷이 다시 라우팅 DB를 검사하지 않도록 함 (경로가 틀리지 않은 경우)
      • 캐시된 라우팅 정보는 INET sock 구조체에서 ip_route_cache 에 의해 참조됨
      • UDP는 sock 상태를 TCP_ESTABLISHED 로 변경
    • TCP는 접속 정보를 가진 메시지를 하나 생성해 목적지로 전달하며, 이 메시지는 시작 메시지 순서 번호와 시작하는 호스트에서 처리할 수 있는 메시지 최대 크기, 송수신 윈도우 크기(아직 보내지 않은 메시지들 중 보관 가능한 메시지의 수) 등을 포함
      • 최대 메시지 크기(MTU)는 요청을 시작한 쪽에서 사용하고 있는 네트워크 장치에 따라 바뀜
      • 받는 쪽의 네트워크 장치가 이보다 작은 최대 메시지 크기를 지원하면 접속중 최소 MTU 값을 사용
    • TCP에서는 모든 메시지에 번호가 붙으며, 초기 순서 번호는 첫번째 메시지 번호와 같고, 악의적인 프로토콜 공격을 피하기 위해 허용 범위 내에서 임의값으로 번호를 지정
    • TCP에서 메시지를 수신하는 호스트는 모든 메시지에 대해 성공적으로 도착하였다는 응답을 송신지로 전달. 만약 이 응답이 없으면 송신측에서 같은 메시지를 재전송
    • TCP 접속 요청을 원하는 프로세스는 요청이 accept/reject 중 하나라는 응답을 받을 때까지 블락되며, 타이머를 걸어 타임아웃시 프로세스가 실행되도록 할 수 있음
    • TCP는 메시지가 들어올 때까지 대기하며, tcp_listening_hash 를 추가하여 들어온 메시지가 sock 구조체로 전달되게 함
    • 클라이언트가 연결을 요청하고, 서버가 요청을 수락하는 과정에서 TCP는 총 3개의 패킷을 주고 받음
      • 이를 3-Way Handshaking 이라고 함
    • 클라이언트가 연결 종료를 요청하고, 서버가 요청을 받고 소켓을 종료하는 과정에서 TCP는 총 4개의 패킷을 주고 받음
      • 이를 4-Way Handshaking 이라고 함

3-way handshaking
https://asfirstalways.tistory.com/356

  • listen: INET BSD 소켓에서 접속 요청 대기
    • 소켓에 주소를 바인딩 했으면, 바인드한 주소를 지정하여 들어오는 접속 요청을 대기하는데, listen 함수는 소켓의 상태를 TCP_LISTEN 으로 바꾸고 들어오는 접속을 허가하기 위해 필요한 특수 작업을 처리
    • 먼저 주소를 바인드하지 않고 접속을 기다릴 수 있으며, 여기서 INET 소켓 계층은 사용하지 않는 포트 번호를 찾아 소켓에 지정
    • UDP는 소켓 상태만 변경하는 반면, TCP는 sock 구조체를 두 개의 해시 테이블(tcp_bound_hash, tcp_listening_hash)에 추가 (두 해시 테이블 모두 IP 포트 번호에 기반한 해시 함수를 통해 인덱싱)
    • listen 소켓은 TCP 접속 요청을 받으면 이를 나타내는 sock 구조체를 생성하고, 접속 요청을 포함한 sk_buff 구조체를 복사하여 sock 구조체의 receive_queue 뒤에 이를 추가
    • 복사한 sk_buff 는 새로 만든 sock 구조체에 대한 포인터를 저장

https://myaut.github.io/dtrace-stap-book/kernel/net.html

  • accept: 접속 요청 허가
    • INET 소켓 접속을 허락하는 것은 TCP 프로토콜에만 적용 (UDP는 해당 없음)
    • listen 소켓 외에 accept/reject 을 위한 소켓에서 socket 구조체를 복사하여 새 socket 구조체를 생성
    • 지원하는 프로토콜 계층(TCP)에 허가하라는 명령을 전달
    • 블럭킹 모드가 아닌 경우, 들어오는 접속이 없으면, 위 작업은 실패하며 새로 만들어진 socket 구조체는 제거됨
    • 블럭킹 모드이면, 접속을 허가하는 프로세스는 대기 큐에 추가되고, 요청을 받을 때까지 중단됨
    • 클라이언트는 서버로부터 ACK 받으면 다시 ACK(접속을 허용 해달라는 메시지)를 보내는데, 이때 sk_buff 구조체는 무시되고, sock 구조체는 이전에 새로 만든 socket 구조체와 연결된 INET 소켓 계층으로 반환됨
    • 새로 생성된 소켓의 파일 기술자(fd)를 프로세스에게 돌려주고, 프로세스는 새 BSD 소켓을 가지고 작업할 때마다 fd를 사용

TCP 연결 과정
https://myaut.github.io/dtrace-stap-book/kernel/net.html

IP 계층

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • 소켓 버퍼(Socket Buffer)
    • 여러 네트워크 프로토콜 계층을 가지면, 각 계층에서 다른 계층의 서비스를 사용하게 되는데, 전송할 때는 데이터에 헤더와 테일을 붙이고, 받을 때는 데이터에서 헤더와 테일을 제거해야 하는 오버헤드가 존재
    • 패킷에서 프로토콜 헤더와 테일을 탐색해야 하므로, 프로토콜 사이에 데이터 버퍼를 전달하는 것을 어렵게 함 (단순히 버퍼를 복사하는 것은 매우 비효율적)
    • 리눅스는 프로토콜 계층 사이에서 네트워크 디바이스 드라이버 간에 데이터를 주고 받기 위해, sk_buff 구조체의 리스트 포인터를 이용하는 소켓 버퍼를 사용
    • 소켓 버퍼는 포인터를 이용해 각 프로토콜 계층이 표준 함수로 프로그램 데이터를 다룰 수 있게 함
    • 각 sk_buff 구조체는 각자의 데이터 블럭을 가지며, 데이터를 다루고 관리하기 위해 4개의 데이터 포인터를 포함
      • head 메모리에서 데이터의 시작 위치를 참조 (블럭 할당 시 고정)
      • data 현재 프로토콜 데이터의 시작 위치를 참조 (현재 sk_buff 구조체를 소유하고 있는 프로토콜 계층에 따라 바뀜)
      • tail 현재 프로토콜 데이터의 끝 위치를 참조 (현재 sk_buff 구조체를 소유하고 있는 프로토콜 계층에 따라 바뀜)
      • end 메모리에서 데이터의 끝 위치를 참조 (블럭 할당 시 고정)
    • sk_buff 구조체를 다루는 코드는 데이터에 헤더와 테일을 붙이고 제거하는 표준 함수를 제공
      • len 현재 프로토콜 패킷의 길이 (data ~ tail)
      • truesize 데이터 버퍼의 전체 크기 (head ~ end)
      • push() data 포인터를 데이터 영역의 시작 쪽으로 이동시키고, len 값을 증가시킴. 전송할 패킷의 시작 부분에 데이터나 프로토콜 헤더를 추가하는데 사용
      • pull() data 포인터를 데이터 영역의 끝 쪽으로 이동시키고, len 값을 감소시킴. 수신한 패킷의 시작 부분에서 데이터나 프로토콜 헤더를 제거하는데 사용
      • put() tail 포인터를 데이터 영역의 끝 쪽으로 이동시키고, len 값을 증가시킴. 전송할 패킷의 끝에 데이터나 프로토콜 정보를 추가하는데 사용
      • trim() tail 포인터를 데이터 영역의 시작 쪽으로 이동시키고, len 값을 감소시킴. 수신한 패킷의 끝 부분에서 데이터나 프로토콜 정보를 제거하는데 사용

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • 디바이스 드라이버 초기화의 결과는 dev_base 리스트에서 서로 연결되어 있는 일련의 device 구조체들로, 각 구조체는 장치를 기술하고 네트워크 프로토콜 계층에서 드라이버가 작업할 때 호출하는 콜백 함수 집합을 제공
    • 이들 함수들은 대부분 데이터 전송 및 네트워크 장치 주소와 관련된 것

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • IP 패킷 수신
    • 네트워크 장치가 패킷을 수신하면 이들은 sk_buff 구조체로 변환되어, backlog 큐에 sk_buff 구조체가 추가됨
      • 만약 backlog 큐가 너무 커지면, 패킷을 무시
    • 이후 실행 준비가 되었음을 네트워크 하반부(bottom half)에 표시하고, 스케줄러는 하반부 핸들러를 실행하는데, 이 핸들러는 backlog 큐를 처리하기 전에 수신한 패킷을 전달할 프로토콜 계층을 결정하며, 전송 대기 중인 패킷들을 처리
    • 네트워크 계층은 초기화될 때, 각 프로토콜은 packet_type 구조체를 ptype_all 리스트나 ptype_base 해시 테이블에 추가하여 자신을 커널에 등록
      • packet_type 구조체는 프로토콜 타입, 네트워크 장치에 대한 포인터, 프로토콜의 수신 데이터 처리 루틴 및 리스트나 해시 테이블에 있는 다음 packet_type 구조체에 대한 포인터를 포함
      • ptype_base 해시 테이블은 프로토콜 식별자로 인덱싱되어, 수신한 패킷을 받을 프로토콜을 결정하기 위해 사용됨
    • 네트워크 하반부 핸들러는 들어오는 sk_buff 구조체의 프로토콜 타입과 ptype_base 해시 테이블에 있는 하나 이상의 packet_type 엔트리와 비교 (반드시 프로토콜은 하나 이상의 엔트리와 매칭됨)

https://www.slideshare.net/hugolu/the-linux-networking-architecture

  • IP 패킷 송신
    • 프로그램이 전송할 데이터를 생성하면, 데이터를 포함한 sk_buff 구조체가 만들어지고, 각 프로토콜 계층을 위에서 아래로 통과하면서 계층별 헤더가 추가됨
    • sk_buff 구조체는 전송할 네트워크 장치로 전달되며, IP 같은 프로토콜은 사용할 장치를 결정
    • 패킷은 루프백 장치를 통해 PPP 모뎀 연결의 끝에 있는 게이트웨이 또는 로컬 호스트 둘 중 하나로 전달됨
    • IP 패킷을 전송할 때는 도달할 IP 주소로 가는 루트(route)를 결정하기 위해 라우팅 테이블(routing table)이 사용되며, 각 IP 목적지는 자신의 라우팅 테이블로부터 다음 도착지를 알아내 루트를 기술하는 rtable 구조체를 반환
      • 이 구조체는 사용할 출발지 IP 주소, 네트워크 device 구조체의 주소, 간혹 미리 만들어진 하드웨어 헤더를 포함
      • 하드웨어 헤더는 네트워크 장치마다 다르며, 출발지와 도착지의 하드웨어 주소와 매개체 별로 다른 정보를 포함
      • 예를 들어, 이더넷 장치이면 출발지/도착지 주소는 물리적 주소(이던세 주소)
    • 하드웨어 헤더는 루트와 함께 캐시되는데, 이 헤더가 해당 루트를 통하여 전송하는 모든 IP 패킷에 추가되는 과정을 줄임
    • 하드웨어 헤더는 ARP 프로토콜로 해결(resolve)되어야 하는 물리 주소가 필요할 수 있는데, 패킷은 주소가 해결될 때까지 블락됨 (한 번 ARP를 요청하면 캐싱하므로 재요청이 필요 없음)
  • 데이터 조각내기(Data Fragmentation)
    • 모든 네트워크 장치는 최대 패킷 크기(MTU)를 가지기 때문에 이보다 큰 크기의 데이터를 보내거나 받기 위해 IP 프로토콜은 데이터를 처리 가능한 크기로 나눔
    • IP 패킷을 보내기 위해, IP 라우팅 테이블에서 네트워크 장치를 찾고, 패킷 크기가 MTU 보다 크면 크기에 맞춰 데이터를 조각내서 각 sk_buff 구조체를 전달 (패킷을 쪼개는 도중에 IP가 sk_buff 구조체를 할당받지 못한다면 전송은 실패)
    • IP 패킷 헤더는 플래그와 이 조각의 오프셋을 가리키는 항목을 포함하며, 마지막 패킷은 마지막 IP 조각이라고 표시
    •  IP 패킷을 받기 위해, IP 패킷 조각이 임의의 순서로 도착하는 점을 고려하여 모두 받은 뒤 재조립
      • 수신할 때마다 패킷 조각인지 검사하고, 처음 받은 경우 새 ipq 구조체를 생성
      • ipq 구조체는 재조립을 기다리는 IP 패킷 조각 리스트인 ipqueue 에 추가됨
      • IP 패킷 조각을 받을 때마다, 맞는 ipq 구조체를 찾기 위해 이 조각을 나타낼 ipfrag 구조체를 생성함 (ipq 구조체에서 fragments 변수에 저장됨)
      • 각 ipq 구조체는 조각난 IP 수신 프레임을 출발지와 도착지 IP 주소와 함께 유일하게 기술하며, 위 프로토콜 계층의 식별자와 해당 IP 프레임의 식별자를 가짐
      • 모든 조각이 도착하면, 데이터들은 하나의 ipq 구조체에서 리스트로 보관되므로, 이들을 sk_buff 구조체 하나로 병합
      • 병합된 sk_buff 구조체를 다음 프로토콜 계층으로 전달
    • 각 ipq 구조체는 조각이 도착할 때마다 타이머를 시작하는데, 타임아웃되면 ipq 구조체와 이것의 ipfrag 구조체들은 메모리에서 소멸되어 메시지는 전송 중 사라진 것으로 간주 (이들의 재전송은 상위 프로토콜의 책임)

데이터 조각화
https://www.slideshare.net/hugolu/the-linux-networking-architecture

주소 결정 프로토콜(Address Resolution Protocol, ARP)

  • IP 주소에서 이더넷 주소 같은 하드웨어 주소로의 변환을 제공하는 프로토콜
  • IP는 디바이스 드라이버에게 데이터를 sk_buff 구조체로 전달하기 전에 ARP를 호출해서 하드웨어 주소를 설정
  • 이 장치가 하드웨어 헤더를 필요로 한다면, 패킷용으로 하드웨어 헤더를 다시 만들어야 하는지 알기 위해 여러 검사를 수행
  • 리눅스는 하드웨어 헤더를 자주 만들지 않도록 캐시를 사용하며, 다시 만들어야 한다면 장치 고유의 하드웨어 헤더 재생성 루틴을 호출
  • 모든 이더넷 장치는 동일한 헤더 재생성 루틴을 호출하며, 이 루틴은 목적지 IP 주소를 물리 주소로 바꾸기 위한 ARP 서비스를 사용
  • ARP 요청과 응답 두 가지 메시지 형태가 존재: 요청은 변환할 IP 주소를 가지며, 응답은 하드웨어 주소(변환된 IP 주소)를 가짐
  • ARP 요청은 네트워크에 연결된 모든 호스트로 전달(브로드캐스트)되므로, 이더넷에 연결된 모든 호스트들이 ARP 요청을 수신하게 되는데 해당 IP 주소의 호스트만 응답

https://slidetodoc.com/chapter-8-arp-and-rarp-objectives-upon-completion/

  • ARP 계층은 각 IP 주소에서 하드웨어 주소로의 변환을 나타내는 arp_table 자료구조를 가지며, 각 엔트리는 IP 주소가 변환될 필요가 있을 때 만들어지고 시간이 지나면서 제거됨
    • last used arp_table 엔트리가 마지막으로 사용된 시간
    • last updated arp_table 엔트리가 마지막으로 수정된 시간
    • flags arp_table 엔트리가 완료되었는지 같은 상태를 나타내는 플래그
    • ip address 엔트리가 나타내는 IP 주소
    • hardware address 변환된 하드웨어 주소
    • hardware header 캐시된 하드웨어 헤더에 대한 포인터
    • timer 응답하지 않는 요구를 타임아웃시키기 위한 timer_list 리스트의 엔트리
    • retries ARP 요청을 재시도한 횟수
    • sk_buff queue 이 IP 주소를 해결(resolve)하길 기다리는 sk_buff 구조체들의 리스트
  • arp_tables 배열은 arp_table의 엔트리들에 대한 캐시로, 각 엔트리는 IP 주소의 끝 두 바이트를 가져와 인덱싱되며, 원하는 엔트리를 찾기 위해 해시 테이블에서 배열을 인덱싱하여 얻은 리스트를 순회
  • 미리 만들어진 하드웨어 헤더의 경우 hh_cache 구조체로 arp_table 엔트리에 캐시
  • 일치하는 arp_table 엔트리가 없는 경우
    • ARP 요청 메시지를 브로드캐스트로 전달하고 타이머를 실행
    • 새 arp_table 엔트리를 생성한 후, 주소 변환을 필요로 하는 패킷들을 엔트리의 리스트(sk_buff queue)에 추가
    • ARP 응답이 없다면, 여러번 요청을 재시도 (여전히 응답이 없으면 arp_table에서 엔트리는 제거되고 큐된 sk_buff 구조체는 실패로 처리 --> TCP는 성립된 TCP 연결을 통해 패킷을 재전송하려고 시도)
    • ARP 응답이 있다면, arp_table 엔트리는 완료된 것으로 표시되고 큐에서 sk_buff 구조체들을 제거
    • 하드웨어 주소는 각 sk_buff 구조체의 하드웨어 헤더에 기록됨
  • ARP 계층은 자신의 프로토콜 타입(ETH_P_ARP)을 커널 네트워크 자료구조에 등록하고, packet_type 구조체를 생성
  • ARP 계층은 네트워크 장치가 수신한 모든 ARP 패킷을 전달받으며, ARP 요청에는 자신의 IP 주소가 지정되어 있으면 반드시 응답
  • 수신한 장치의 device 구조체에 저장되어 있는 하드웨어 주소로 ARP 응답을 생성
  • 네트워크 구성은 시간에 따라 바뀔 수 있으며, IP 주소는 다른 하드웨어 주소로 재할당 될 수 있음. 예를 들어, 전화접속 서비스는 연결될 때마다 각각 다른 IP 주소를 배정 받음
  • arp_tables 캐시가 항상 가장 최근의 엔트리를 가질 수 있도록, 정기적인 타이머를 실행해 모든 엔트리들이 타임아웃되지 않았는지 검사. 만약 하나 이상의 캐시된 하드웨어 헤더를 가지는 엔트리들(다른 자료구조들과 의존 관계)은 제거되지 않도록 주의
  • arp_table 엔트리 중 몇몇은 영구적이며, 이들은 할당이 해제되지 않도록 별도로 표시됨
  • 각 엔트리는 커널 메모리에 저장되기 때문에 너쿠 커지지 않도록 크기가 최대값에 도달할 때마다 가장 오래된 엔트리들을 제거

https://slidetodoc.com/chapter-8-arp-and-rarp-objectives-upon-completion/

IP 라우팅

  • IP 라우팅 함수는 특정 IP 주소를 목적지로 하는 IP 패킷의 다음 도착지를 결정
  • 목적지에 도착 가능 유무, 전송하는데 사용할 네트워크 장치, 장치의 성능 등에 관한 정보를 관리하는 IP 라우팅 DB가 2개 존재
  • 전달 정보 데이터베이스(Forwarding Information Database) IP 주소와 알려진 루트(route)의 목록을 저장
    • 각 IP 서브넷은 fib_zone 구조체로 표현되며, 이들 모두는 fib_zones 해시 테이블에서 참조
    • 해시 값은 IP 서브넷 마스크로 생성되며, 동일한 서브넷으로의 모든 루트들은 fib_node 쌍으로 기술됨
    • fib_info 구조체는 각 fib_zone 구조체의 fz_list 큐에 추가됨
    • 만약 이 서브넷에 있는 루트 개수가 많아지면, fib_node 구조체를 쉽게 찾도록 해시 테이블이 생성됨
    • 서브넷으로 가는 루트가 여러 개 있다면, 각 루트는 다른 게이트웨이를 사용해야 함
    • 한 루트의 거리는 도달해야 하는 서브넷까지 거쳐야 하는 IP 서브넷의 개수를 의미 (이 값이 클수록 좋지 않은 루트)
    • 루트는 BSD 소켓 인터페이스로 IOCTL 요청을 보내서 추가되거나 제거 될 수 있음
      • 네트워크 구성이 시간이 지남에 따라 바뀌면 루트도 동적으로 변할 수 있음
    • 슈퍼유저 권한을 프로세스만이 INET 프로토콜 계층에서 IP 루트를 추가 및 제거 할 수 있음
    • 대부분 시스템은 라우터가 아닌 단말 시스템의 경우, 고정된 루트를 사용
    • 라우팅 프로토콜은 GATED 같은 데몬으로 구현되어 있으며, IOCTL 요청을 보내서 루트를 추가하거나 삭제

  • 목적지로 가는 루트를 빨리 찾기 위해, 더 작고 더 빠른 DB인 루트 캐시(route cache)를 사용
    • 루트 캐시는 자주 접근하는 루트에 대한 것들만 저장하며, ip_rt_hash_table 자료구조로 표현
    • 이 해시 테이블은 rtable 구조체의 리스트에 대한 포인터를 가진 배열로, 해시 값은 IP 주소 하위 두 바이트로 계산됨
    • 두 바이트는 목적지마다 다르기 때문에 해시 값을 가장 잘 분산시켜줌
    • 각 rtable 엔트리는 루트에 대한 정보를 포함: 목적지 IP 주소, 이 주소에 도달하는데 사용할 네트워크 장치, 메시지의 최대 크기, 참조 횟수, 사용 횟수, 타임스탬프 등
    • 참조 횟수는 이 루트가 사용될 때마다 증가하는 값으로, 이 루트를 사용하는 네트워크 연결의 개수를 의미. 프로세스가 이 루트로 데이터를 주고받지 않게 되면 감소
    • 사용 횟수는 이 루트를 발견할 때마다 증가하며, rtable 리스트에서 엔트리의 순서를 결정하는데 사용
    • 타임스탬프는 마지막으로 사용한 시간 값으로, 엔트리가 너무 오래되지 않았는지 검사하는데 사용
  • IP 루트를 조회하면 일치하는 루트를 찾기 위해 루트 캐시를 먼저 검사
  • 루트 캐시에 일치하는 루트가 없다면, 전달 정보 데이터베이스에서 루트를 탐색
  • 어떤 루트도 찾을 수 없다면, IP 패킷은 전송 실패로 처리되어 프로세스에게 전달됨
  • 루트가 전달 정보 데이터베이스에 있고 루트 캐시에 없다면, 이 루트에 해당하는 새 엔트리를 만들어 루트 캐시에 추가
  • 루트 캐시는 LRU 방식으로 동작: 최근에 많이 사용되지 않은 엔트리는 루트 캐시에서 제거하고, 만약 찾는 루트가 캐시에 있다면 리스트 맨 앞에 오도록 배치 (가장 최근에 많이 접한 엔트리가 항상 앞에 오도록 함)

 

본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.


리눅스 파일 시스템

  • 리눅스는 각 파일 시스템이 계층적인 트리 구조로 통합해서 나타내므로, 파일 시스템이 하나인 것처럼 보여줌
    • 윈도우즈는 드라이브 이름 등의 장치 식별자로 구분
    • 새로운 파일 시스템을 마운트하면 하나의 파일 시스템에 추가된 형태로 보여짐
    • 파일 시스템은 로컬 시스템이 아닌 네트워크 연결로 원격지에서 마운트된 디스크도 포함 가능
  • 모든 파일 시스템은 어떤 타입이든지 하나의 디렉토리에 마운트되어, 마운트된 파일 시스템의 파일들이 그 디렉토리의 내용을 구성
    • 이는 많은 파일 시스템을 지원할 수 있게 함
    • 이러한 디렉토리를 마운트 디렉토리 또는 마운트 포인트라고 부름
  • 파일 시스템의 마운트가 해제되면, 마운트 디렉토리가 원래 가지고 있던 파일들이 보여짐
  • 디스크가 초기화될 때, 파티션 구조를 가지며 물리 디스크를 논리적으로 나누는 작업을 파티셔닝(partitioning)이라고 함
  • 각 파티션은 하나의 파일 시스템을 가지며, 파일 시스템은 장치의 블럭에 파일을 디렉토리나 소프트 링크(윈도우즈의 바로가기 같은) 등과 함께 논리적인 계층 구조로 구성 (블럭 장치만이 파일 시스템을 저장할 수 있음)
  • 파일 시스템은 일련의 블럭 장치들의 집합이며, 운영체제는 물리 디스크가 어떤 형태(구조)인지 알 필요가 없음
    • 하드웨어의 추상화 단계로 물리 장치의 세부 사항을 알 필요가 없음
    • 블럭을 읽는 요청은 각 블럭 디바이스 드라이버의 책임(블럭이 읽어야 하는 위치를 매핑)으로, 파일 시스템은 어떤 장치에 블럭이 있는지 상관없이 동작함
  • 파일(file)이란 데이터의 집합이며, 파일에 담긴 데이터 외에 파일 시스템의 구조도 포함되는데, 이러한 정보들은 신뢰성있게 저장되어야 하며 파일 시스템은 무결성을 보장해야 함
  • 처음에 제안된 파일 시스템은 확장 파일 시스템(Extended File System, EXT)으로, 가상 파일 시스템(Virtual File System, VFS)이라는 인터페이스 계층을 통해 실제 파일 시스템이 운영체제와 운영체제의 서비스로부터 분리됨
    • 즉, 파일 시스템의 세부 사항들이 소프트웨어에 의해 변환되는 것이고, VFS를 지원하는 파일 시스템들은 모두 사용 가능
  • VFS는 마운트되어 사용중인 각 파일 시스템의 정보를 메모리에 캐싱하며, 파일이나 디렉토리가 생성, 삭제, 또는 자료가 입력될 때 캐시 안의 자료를 정확하게 수정함. 이는 메모리 내 캐시와 디스크의 내용의 일치성을 보장함
    • 이 과정에서 디바이스 드라이버에 의해 접근하려는 파일이나 디렉토리 관련 자료구조가 생성되거나 제거됨
  • 버퍼 캐시(Buffer Cache)는 각 파일 시스템이 블럭 장치에 접근할 때 사용되며, 블럭에 접근할 때마다 그 블럭은 버퍼 캐시에 들어가고 상태에 따라 다양한 큐에 추가됨. 버퍼 캐시는 데이터 버퍼를 캐시할 뿐만 아니라, 블럭 디바이스 드라이버와의 비동기적인 인터페이스 관리에도 도움이 됨.
  • EXT가 성능이 떨어지면서 2차 확장 파일 시스템(EXT2)가 등장

https://stackoverflow.com/questions/34253611/are-device-files-implemented-by-the-specific-file-systems-or-the-virtual-file-sy
공통 파일 시스템 인터페이스
https://scslab-intern.gitbooks.io/linux-kernel-hacking/content/chapter13.html

2차 확장 파일 시스템(Second Extended File System, EXT2)

EXT2 파일 시스템의 물리적 배치도

  • EXT2의 파일에 저장된 데이터는 블럭에 저장되는데, 데이터 블럭의 크기는 동일하며, 서로 다른 EXT2의 경우 크기가 다를 수 있음
  • 블럭 크기는 파일 시스템이 만들어질 때 결정되며, 모든 파일의 크기는 블럭의 크기에 따라 올림이 됨
    • 예를 들어, 블럭 크기가 1024바이트이고, 파일 크기가 1025바이트일 때, 이 파일은 1024바이트 블럭 2개를 차지함
    • 실제로 파일 하나당 평균 블럭 크기의 절반을 낭비하는데, 이는 CPU의 메모리 사용량과 디스크 공간의 활용도 사이에 트레이드 오프(trade off) 문제로, 리눅스는 CPU 부담을 줄이는 방향으로 디스크 공간의 활용도를 희생함
  • 파일 시스템의 모든 블럭이 데이터만 저장하는 것은 아니며, 몇몇 블럭에는 파일 시스템의 구조를 표현하는 정보가 있음
  • 파티션은 하나의 파일 시스템을 가지는데, 파일 시스템은 공간을 블럭 그룹으로 쪼개고, 각 블럭 그룹은 파일 시스템에서 무결성을 보장해야 하는 정보를 중복해서 갖고 있어 파일 시스템 복구 시 중복 정보를 이용
  • EXT2 inode
    • EXT2는 파일 시스템 배치도를 정의하기 위해 시스템 내의 각 파일을 inode 자료구조로 표현하며, 모든 inode는 inode 테이블에 저장되고, 파일은 일반 파일, 디렉토리, 소프트 링크, 하드 링크, 장치 파일을 포함
    • 즉, 디렉토리도 inode로 기술되며, 디렉토리에 속하는 파일들의 inode 포인터를 디렉토리 inode에서 보관
    • inode는 파일의 데이터가 위치한 블럭과 파일에 대한 접근 권한 및 파일 수정 시간, 파일 종류 등의 정보를 가지며, EXT2의 모든 파일은 각각 하나의 inode와 대응 (즉, inode는 구분 가능한 고유 번호를 가지고 식별됨)
    • Mode 접근 권한 정보 및 파일의 유형(일반 파일, 디렉토리, 심볼릭 링크, 블럭 장치, 문자 장치, FIFO)
    • Owner Information 해당 파일의 소유자 및 그룹 식별자
    • Size 바이트 단위의 파일 크기
    • Timestamp inode가 만들어진 시각과 최종 수정 시각
    • Datablocks 데이터가 저장된 블럭에 대한 포인터, 맨 앞 12개의 포인터는 데이터를 저장한 실제 블럭에 대한 포인터이고 마지막 3개는 점점 더 높은 수준의 간접 연결(블럭의 포인터의 포인터)을 포함.
    • 특수 장치 파일은 실제 디스크에 존재하지 않는 파일이지만 inode로 표현되며, 커널 내부에는 장치를 접근하기 위한 코드가 존재

  • EXT2 Superblock
    • 수퍼블럭은 파일 시스템의 기본 크기나 모양에 대한 설명을 기술하는 자료구조로, 이 정보를 이용하여 관리자가 파일 시스템을 활용하고 유지
    • 보통 파일 시스템이 마운트 될 때, 블럭 그룹 0에 있는 수퍼블럭을 읽어들이지만, 모든 블럭 그룹에는 동일한 복사본이 존재해 파일 시스템이 깨지는 경우를 대비함
    • Magic Number 마운트 프로그램에 EXT2의 수퍼블럭이라는 것을 알리는 용도 (=0xEF53)
    • Revision Level 메이저 개정 레벨과 마이너 개정 레벨로 구성되며, 마운트 프로그램이 어떤 특정 버전에서만 지원되는 기능이 현재 파일 시스템에서 지원되는지 확인하는 용도. 또한 마운트 프로그램이 파일 시스템에서 안전하게 사용가능한 기능 목록(기능 호환성 항목)을 판단할 때 사용
    • Mount Count & Maximum Mount Count 파일 시스템 전부를 검사할 필요가 있는지 확인할 때 사용. 마운트 횟수는 파일 시스템이 마운트될 때마다 1씩 증가하며, 그 값이 최대 마운트 횟수에 도달하면 e2fsck를 실행하라는 메시지가 출력됨
    • Block Group Number 현재 수퍼블럭 복제본을 가지는 블럭 그룹의 번호
    • Block Size 바이트 단위의 블럭 크기
    • Blocks per Group 그룹당 블럭 수 (파일 시스템 만들 때 지정됨)
    • Free Blocks 파일 시스템 내 프리 블럭 수 = 사용 가능한 블럭 수
    • Free Inode 파일 시스템 내 프리 inode 수
    • First Inode 첫번째 inode 번호, 루트 파일 시스템에 루트 디렉토리를 나타냄

  • EXT2 Group Descriptor
    • 각 블럭 그룹은 자신을 기술하는 자료구조를 가지며, 수퍼블럭처럼 모든 블럭 그룹을 위한 그룹 기술자는 각 블럭 그룹에 복제되어 있어 파일 시스템이 깨지는 경우를 대비 (실제로 블럭 그룹 0에 있는 첫번째 복사본만 사용됨)
    • Block Bitmap 블럭 그룹에서 블럭의 할당 상태를 나타내는 비트맵 (블럭 수만큼 존재, 블럭 할당 및 해제 시 사용)
    • Inode Bitmap 블럭 그룹에서 inode의 할당 상태를 나타내는 비트맵 (블럭 수만큼 존재, inode 할당 및 해제 시 사용)
    • Inode Table 블럭 그룹의 inode 테이블의 시작 블럭 (블럭 수만큼 존재, 각 inode는 EXT2 inode 구조체에 의해 표현)
    • Free Blocks Count & Free Inode Count 
    • Used Directory Count 
    • 그룹 기술자는 전체적으로 하나의 그룹 기술자 테이블을 형성하며, 각 블럭 그룹에는 수퍼블럭 뒤에 그룹 기술자 테이블이 존재
  • EXT2 Directory
    • 디렉토리는 파일에 대한 접근 경로를 생성하고 저장하는 특별한 파일
    • 메모리 상에서 디렉토리 파일은 디렉토리 엔트리의 리스트
    • 디렉토리 엔트리는 디렉토리에 저장된 파일과 1:1 대응이며, 모든 디렉토리에서 첫 2개의 엔트리는 각각 .과 ..으로 현재 디렉토리와 부모 디렉토리를 의미
    • Inode 엔트리에 해당하는 Inode로, 이 값은 블럭 그룹의 inode 테이블에 저장되어 있는 inode 배열에 대한 인덱스
    • name length 바이트 단위의 파일 이름 길이
    • name 파일 이름

디렉토리 엔트리

EXT2 파일 연산

  • 파일 탐색
    • 파일 이름은 길이 제한이 없고 인쇄 가능한 문자면 가능. 단, 전체 경로에서 파일을 구분하기 위해 슬래시(/)를 사용
    • 파일을 나타내는 inode를 찾기 위해 시스템은 파일 이름을 해석해서 한 디렉토리씩 처리하면서 마지막 슬래시 뒤에 있는 이름을 가진 파일을 얻음
    • 루트 디렉토리의 inode 번호는 파일 시스템의 수퍼블럭에서 얻으며, 이 번호의 파일을 읽기 위해 해당 블럭 그룹의 inode 테이블을 이 번호로 인덱싱하여 엔트리를 얻음
    • 정리하자면, 전체 경로에서 파일을 찾을 때까지 디렉토리 엔트리를 반복적으로 찾아가면서 마지막 슬래시 뒤에 이름을 갖는 파일의 데이터 블럭을 얻음
  • 파일 크기 변경
    • 파일 시스템은 데이터를 가지고 있는 블럭들이 분할되는 경향(디스크 조각화)이 있는데, EXT2의 경우 어떤 파일에 대한 새로운 블럭을 현재 데이터 블럭들에 인접하도록 할당하거나 적어도 현재 블럭 그룹과 같은 그룹에 할당하는 것으로 이 문제를 극복
      • 둘 다 실패하면, 다른 블럭 그룹에 있는 데이터 블럭을 할당 (어쩔 수 없음)
    • 프로세스가 파일에 데이터를 쓰려고 할 때마다, 파일 시스템은 데이터가 파일에 마지막으로 할당된 블럭을 넘어가는지 검사하고, 넘어가면, 이 파일을 위한 새로운 데이터 블럭을 할당 (프로세스는 할당하고 기록이 끝날 때까지 대기)
      • 블럭 할당 루틴은 파일 시스템의 수퍼블럭에 락을 걸어 수퍼블럭에 있는 항목을 변경. 이는 둘 이상의 프로세스가 파일 시스템을 동시에 변경하는 것을 막기 위한 것으로, 다른 프로세스가 블럭을 할당하려고 하면 현재 프로세스는 대기
      • 수퍼블럭의 사용은 요청 순서에 따르며, 한 프로세스가 제어를 갖게 되면 작업을 종료할 때까지 제어를 가짐(원자적)
      • 수퍼블럭에 락을 걸고 나면, 프리 블럭(사용가능한 블럭)이 충분한지 검사. 만약 충분하지 않다면 할당 루틴은 실패하고 프로세스는 수퍼블럭에 대한 제어권을 양도
      • 만약 프리 블럭이 충분하다면, 프로세스는 새 블럭을 할당 받고 수퍼블럭에 대한 제어권을 양도
      • 미리 할당된 블럭을 사용할 수 있는데, 실제 존재하지는 않으며 단지 할당된 블럭 비트맵에 예약되어 있음
    • inode에는 새로운 데이터 블럭을 할당하기 위한 항목 2개가 존재
      • prealloc_block 처음에 미리 할당된 데이터 블럭 수
      • prealloc_count 그 중에서 남은 개수
    • 할당할 때는 마지막 데이터 블럭의 다음 데이터 블럭이 비었는지 검사해서 비었으면 할당하고 안비었으면 검색 범위를 넓혀 가까운 블럭에서 64블럭 이내의 데이터 블럭을 살펴봄 (순차 접근을 빠르게 하기 위함)
      • 대부분 그 파일에 속한 다른 데이터 블럭과 같은 블럭 그룹
      • 빈 블럭이 없다면 계속 탐색하는데, 한 블럭 그룹 안에 8개의 빈 데이터 블럭으로 된 덩어리를 찾으려고 함
      • 만약 블럭 미리 할당 기능이 필요하고 사용 가능할 경우, prealloc_block 과 prealloc_count 값을 각각 갱신
    • 빈 블럭을 찾을 때마다 블럭 그룹의 블럭 비트맵을 갱신하고 버퍼 캐시 내에 데이터 버퍼를 할당
      • 데이터 버퍼는 파일 시스템을 지원하는 장치 식별자와 할당된 블럭의 번호에 의해 식별됨
      • 버퍼 내의 데이터가 모두 0이고 버퍼가 더티(dirty)라고 표시되어 있으면 실제 디스크에 내용이 기록되지 않은 것
    • 수퍼블럭을 기다리는 프로세스가 있으면 큐에 있는 프로세스 중 첫번째 프로세스가 다시 실행되며 파일 처리에 필요한 수퍼블럭을 독점

가상 파일 시스템(Virtual File System, VFS)

VFS 구조

  • 전체 파일 시스템과 특정 마운트된 파일 시스템을 기술하는 자료구조를 관리하기 위한 인터페이스로, EXT2와 비슷한 방법으로 시스템에 있는 파일을 수퍼블럭과 inode로 표현 (VFS inode는 EXT2 inode처럼 시스템에 있는 파일과 디렉토리를 나타냄)
  • 각 파일 시스템들은 부팅 중 초기화될 때, 자신을 VFS에 등록하여 커널에 파일 시스템이 포함될 수 있으며, 특정 파일 시스템을 마운트하는 경우 모듈에 의해 VFS에 등록됨
  • 블럭 장치에 기반한 파일 시스템이 마운트되었고, 루트 파일 시스템이 존재한다면, VFS는 이것의 수퍼블럭을 읽어와 해당 파일 시스템의 배치도를 VFS 수퍼블럭 자료구조에 매핑시킴
  • VFS는 마운트된 파일 시스템과 VFS 수퍼블럭의 리스트를 관리하며, 각각의 VFS 수퍼블럭은 특정 기능을 수행하는 루틴에 대한 정보와 포인터 및 파일 시스템의 첫번째 VFS inode에 대한 포인터를 포함 (루트 파일 시스템의 경우 이 inode는 루트 디렉토리의 것)
    • 예를 들어, 마운트된 파일 시스템을 나타내는 수퍼블럭은 고유의 inode 읽기 루틴에 대한 포인터를 가지며, 호출 시 적절한 파일 시스템의 블럭을 읽을 수 있게 됨
  • 프로세스가 파일이나 디렉토리에 접근할 때, VFS inode를 탐색하는 시스템 루틴이 호출되며, 수많은 inode가 반복적으로 접근
    • 접근 속도를 빠르게 하기 위해 inode는 캐시에 저장됨
    • 어떤 inode가 캐시에 존재하지 않으면, 해당 inode를 읽기 위해(디스크->메모리) 각 파일 시스템 고유의 루틴을 호출
    • 읽어들인 inode는 캐시에 저장되어 다음번 접근 시 캐시에서 탐색됨
    • 덜 사용되는 VFS inode는 캐시로부터 제거 (LRU Cache)
  • 모든 파일 시스템은 실제 장치에 대한 접근 속도를 높이기 위해 버퍼 캐시(Buffer Cache)를 사용
    • 같은 데이터가 자주 필요할 때를 대비하여 디스크 접근 횟수를 줄임
    • 버퍼 캐시는 파일 시스템과는 상호 독립적(비동기 요청 인터페이스를 제공)이며, 커널이 데이터 버퍼를 할당하고 읽고 쓰는 메커니즘에 통합되어 있음
  • inode를 읽기 위해, 커널은 블럭 디바이스 드라이버에게 제어하는 장치로부터 블럭을 읽도록 요청하여 디스크에서 데이터를 가져옴
    • 이러한 블럭 장치 인터페이스는 버퍼 캐시에 통합되어 있음 (버퍼 캐시에서 데이터가 없는 경우 즉시 호출)
  • 파일 시스템이 어떤 블럭을 읽으면 그 블럭은 버퍼 캐시에 저장되어 모든 파일 시스템과 커널에 의하여 공유되며, 버퍼 캐시에 있는 각 데이터 버퍼는 블럭 번호와 그 블럭을 읽은 장치의 고유 식별자에 의하여 구분됨
  • 자주 사용되는 디렉토리의 inode를 빨리 찾기 위해 디렉토리 캐시도 제공, 이 캐시는 전체 디렉토리 이름과 해당되는 inode 번호와의 매핑을 저장하며 디렉토리 자체에 대한 inode는 inode 캐시에 저장됨

http://www.haifux.org/lectures/119/linux-2.4-vfs/linux-2.4-vfs.html

  • VFS Super Block
    • device 파일 시스템이 저장되어 있는 블럭 장치의 식별자
    • mounted inode 파일 시스템의 첫번째 inode를 참조하는 포인터
    • covered inode 파일 시스템이 마운트된 디렉토리를 표현하는 inode를 참조하는 포인터
      • 루트 파일 시스템의 VFS 수퍼블럭은 covered inode 가 없음
    • block size 바이트 단위의 블럭 크기
    • superblock operations 마운트된 파일 시스템의 file_system_type 구조체를 참조하는 포인터
    • file system specific 파일 시스템이 필요로 하는 정보를 참조하는 포인터
  • VFS inode
    • 각 VFS inode의 정보는 파일 시스템의 정보로부터 파일 시스템 고유 루틴에 의해 생성되며, VFS inode는 커널 메모리에만 존재하고 시스템이 필요한 동안 VFS inode 캐시에 저장됨
    • device 해당 VFS inode가 나타내는 파일 또는 장치의 식별자
    • inode 파일 시스템 안에서 유일한 inode 번호, 장치와 inode 번호의 조합은 VFS 내에서 유일
    • mode VFS inode가 해당하는 파일의 종류(파일, 디렉토리, 기타 등)와 적븐 권한 등을 나타냄
    • user id 파일의 소유자
    • times 생성 시각, 변경 시각, 읽은 시각 등을 나타냄
    • block size 바이트 단위의 블럭 크기
    • inode operations 연산 루틴의 주소들에 대한 포인터 (만약 0이면 inode가 프리되어 제거되거나 재사용됨)
    • count VFS inode를 현재 사용하는 시스템 요소(프로세스 등)의 수 (만약 0이면 inode가 프리되어 제거되거나 재사용됨)
    • lock VFS inode를 락 걸기 위해 사용 (inode를 읽을 때)
    • dirty VFS inode가 기록되어 파일 시스템 아래 계층에서 변경이 필요한지 나타냄
    • file system specific information

 

VFS 자료구조
https://myaut.github.io/dtrace-stap-book/kernel/fs.html
https://www.programmersought.com/article/50821832993/

VFS 연산

  • 파일 시스템 등록
    • 커널을 빌드할 때 지원할 파일 시스템을 지정할 수 있음.
    • 파일 시스템 시작 코드는 내장된 모든 파일 시스템의 초기화 루틴을 호출
    • 파일 시스템 모듈은 로드될 때마다 자신을 커널에 등록하고, 언로드될 때 자신을 커널로부터 해제함
    • 각 파일 시스템의 초기화 루틴은 자신을 VFS에 등록하며, file_system_type 구조체로 표현
    • 이 구조체에는 파일 시스템의 이름과 VFS 수퍼블럭 읽기 루틴에 대한 포인터가 저장되어 있음
    • *read_super() 파일 시스템이 마운트될 때 VFS에 의해 호출
    • name e.g., ext2
    • required_dev 이 파일 시스템을 실제로 지원하는 장치가 필요한지를 나타냄
    • 어떤 파일 시스템이 등록되어 있는지는 /proc/filesystems 파일 내용을 출력해서 알 수 있음

  • 파일 시스템 마운트
    1. 수퍼유저가 파일 시스템을 마운트하려고 할 때, 커널은 시스테 콜로 전달된 인자가 옳은지 파악
      • e.g., mount -t iso9660 -o ro /dev/cdrom /mnt/cdrom
      • 커널에 3가지 정보를 전달: 파일 시스템 이름, 파일 시스템을 포함하고 있는 블럭 장치, 마운트할 위치
    2. VFS는 먼저 파일 시스템 리스트(file_systems)를 탐색하여 각 file_system_type 구조체를 살펴보고, 인자로 넘어온 이름의 구조체를 발견하면 커널에서 지원하는 타입인지 검사
      • 일치하는 것을 찾지 못하더라도, 커널이 그 파일 시스템의 모듈을 요구시 로드하도록 빌드되었다면 커널 데몬이 모듈을 로드
    3. 새로운 파일 시스템의 마운트 지점이 될 디렉토리의 VFS inode를 탐색
      • 이 VFS inode는 캐시에 있거나, 마운트 지점의 파일 시스템을 저장하고 있는 블럭 장치에서 읽어옴
    4. inode를 찾으면 디렉토리인지 혹은 이미 다른 파일 시스템이 마운트되었는지 검사
      • 한 디렉토리는 단 하나의 파일 시스템의 마운트 지점으로만 사용 가능
    5. 새로운 VFS 수퍼블럭을 할당하고, 이를 마운트 정보와 함께 해당 파일 시스템을 위한 수퍼블럭 읽기 루틴에 전달
      • 모든 VFS 수퍼블럭은 super_block 구조체의 배열인 super_blocks에 저장됨
      • 이번 마운트를 위해 super_block 구조체는 하나만 할당됨
    6. 수퍼블럭 읽기 루틴은 물리 장치에서 읽은 정보에 따라 VFS 수퍼블럭을 채워넣음
      • 만약 그 파일 시스템의 블럭 장치로부터 읽지 못한다면(블럭 장치가 해당 파일 시스템의 타입으로 사용 불가한 경우) 마운트 명령은 실패
    7. 마운트된 각 파일 시스템은 vfsmount 구조체로 기술되며, 이들은 vfsmntlist 가 참조하는 리스트에 추가됨
      • vfsmnttail 포인터는 리스트의 마지막 항목을 가리키고, mru_vfsmnt 포인터는 가장 최근에 사용된 파일 시스템을 참조
      • 각 vfsmount 구조는 파일 시스템을 담고 있는 블럭 장치의 장치 번호, 이 파일 시스템이 마운트된 디렉토리, 마운트될 때 할당된 VFS 수퍼블럭에 대한 포인터 등을 포함

마운트 커널 자료구조

  • 파일 시스템 마운트 해제
    • 어떤 프로세스도 마운트된 디렉토리나 그 하위 디렉토리를 사용하지 않는 상태에서만 마운트 해제 가능
    • 사용중이면 VFS inode 캐시에 해당 파일 시스템이 속하는 VFS inode가 존재하기 때문에, 캐시의 inode 리스트를 검사
    • 마운트된 파일 시스템의 VFS 수퍼블럭이 더티(dirty) 상태이면, 수퍼블럭을 디스크에 기록
    • 디스크 기록 후, VFS 수퍼블럭이 차지하고 있던 메모리를 커널 메모리 풀(memory pool)에 보내고, 마지막으로 이 파일 시스템의 마운트에 필요했던 vfsmount 구조체를 vfsmntlist 리스트에서 제거한 뒤 메모리 해제

VFS 캐시

  • VFS inode 캐시
    • VFS는 마운트된 파일 시스템에 대한 접근 속도를 높이기 위해 해시 테이블로 구현된 inode 캐시를 유지
    • inode를 접근할 때마다 VFS inode 캐시를 먼저 탐색
    • 해시 테이블 내의 각 엔트리는 같은 해시 값을 갖는 VFS inode 리스트를 참조
    • 해시 값은 inode 번호와 그 파일 시스템을 갖는 장치의 장치 식별자로 계산됨
    • VFS inode 를 얻으려면 찾으려는 것과 같은 inode 번호와 장치 식별자를 가진 inode를 찾을 때까지 리스트를 순회
    • 만약 inode가 있으면, 카운트 값을 증가시키면서 그 inode를 사용하는 프로세스가 있음을 알리고 파일 시스템에 접근
    • 만약 찾을 수 없다면, 파일 시스템이 메모리로 파일을 가져오기 위해 빈 VFS inode를 탐색
    • VFS inode를 더 할당할 수 있으면, 커널 페이지를 할당하고 이를 여러 새 빈 inode로 쪼갠 뒤, 할당된 VFS inode는 first_inode가 가리키는 리스트와 캐시의 리스트에 추가됨
    • VFS inode를 더 할당할 수 없으면, 재사용할만한 inode 후보들을 탐색 (사용횟수가 0인 inode는 좋은 후보)
      • 만약 VFS inode가 더티 상태이면 파일 시스템에 그 내용을 기록
      • 만약 락이 걸려 있으면 풀릴 때까지 대기
      • 재사용 후보가 선택되면 내용을 지움
    • 할당된 VFS inode는 파일 시스템에서 읽어온 정보를 채우는 루틴이 호출되며, 카운트 값은 1이 되고 락이 걸림
      • 그 inode가 완전한 정보를 가지기 전까지 아무도 접근 할 수 없음
    • VFS inode 캐시가 사용되어 꽉 차면, 덜 사용되는 inode는 버려지고 더 많이 사용되는 것들만 남음 (LRU Cache)
  • 디렉토리 캐시
    • VFS는 디렉토리에 대한 접근 속도를 높이기 위해 해시 테이블로 구현된 디렉토리 엔트리에 대한 캐시를 유지
    • 디렉토리는 실제 파일 시스템에 의하여 참조되므로, 그 내용도 디렉토리 캐시에 저장됨
    • 짧은 이름(최대 15자)을 가진 디렉토리 엔트리만 캐시되며, 이는 짧은 것이 더 자주 사용되기 때문
    • 캐시의 테이블 엔트리는 같은 해시 값을 가진 디렉토리 캐시 엔트리들의 리스트를 참조
    • 해시 함수는 파일 시스템을 갖고 있는 장치의 장치 번호 및 디렉토리 이름을 사용하여 해시 테이블 내의 인덱스를 산출
    • 캐시 값을 유효하게 하고 최신 값을 유지하는 LRU 방식으로 디렉토리 캐시 엔트리 리스를 관리
      • 디렉토리 엔트리는 처음에 참조되면 1단계 LRU 리스트의 맨 뒤에 추가
      • 1단계 리스트가 꽉 차서 자리가 없으면, 이 리스트의 맨 앞 엔트리를 제거 (가장 적게 사용된 것)
      • 디렉토리 엔트리가 다시 참조되면 2단계 LRU 리스트로 엔트리를 옮김
      • 2단계 리스트가 꽉 차서 자리가 없으면, 이 리스트의 맨 앞 엔트리를 제거

https://www.researchgate.net/figure/The-Linux-original-path-lookup-mechanism-The-block-means-a-dentry-structure-and-p_fig1_325982529
https://scslab-intern.gitbooks.io/linux-kernel-hacking/content/chapter13.html

  • 버퍼 캐시
    • 디스크에서 데이터 블럭을 읽고 쓸 때 디스크 접근 횟수를 줄이기 위해 해시 테이블로 구현된 블럭 버퍼에 대한 캐시
    • 파일 시스템이 데이터 블럭을 읽고 쓰는 요청을 보내면, 표준 커널 함수를 호출하여 블럭 디바이스 드라이버에게 buffer_head 구조체를 전달. 이 구조체는 블럭 디바이스 드라이버가 필요로 하는 모든 정보를 제공
      • 장치 식별자는 장치를 유일하게 구별해주고, 블록 번호는 읽어야 할 위치를 알려줌
    • 모든 블럭 버퍼들은 새 것이거나 안 쓰이거나 상관없이 버퍼 캐시 어딘가에 존재하며, 모든 블럭 장치들이 이 캐시를 공유
    • 지원하는 버퍼 크기별로 각각 하나의 리스트가 존재하며, 시스템의 프리 블럭 버퍼는 처음 만들어질 때나 블럭이 해제될 때 이 리스트에 추가됨 (현재 지원하는 버퍼 크기는 512, 1024, 2048, 4096, 8192바이트)
    • 버퍼 캐시는 똑같은 해시 값을 가지는 버퍼들의 리스트를 참조하는 포인터들의 배열로, 해시 값은 블럭 버퍼를 소유하는 장치 식별자와 버퍼의 블럭 번호로 산출되며, 각 버퍼 유형마다 LRU 리스트가 있고 캐시에 추가되면 이 리스트에도 추가됨
      • LRU 리스트는 특정 유형의 버퍼에 대해 작업할 때 사용되며, 새로운 데이터를 가진 버퍼를 디스크에 기록
    • 버퍼의 유형은 버퍼의 상태를 반영한 것
      • clean 사용하지 않은 새 버퍼
      • locked 버퍼에 락이 걸려 있으며, 기록되기를 기다리는 버퍼
      • dirty 새롭고 유효한 데이터를 가지며, 언제 기록될지 스케줄되지 않은 버퍼
      • shared 공유 버퍼
      • unshared 이전에 공유되었으나 지금은 공유하지 않는 버퍼
    • 블럭 버퍼는 프리 리스트 중 어떤 하나의 리스트나, 버퍼 캐시 둘 중 하나에만 속함
    • 파일 시스템이 물리 장치로부터 버퍼를 읽을 때마다 가장 먼저 버퍼 캐시에 접근해서 블럭을 얻으려고 시도
      • 만약 버퍼 캐시에서 얻을 수 없다면, 프리 리스트에서 적당한 크기의 버퍼를 하나 얻고, 이는 버퍼 캐시에 추가됨
      • 만약 버퍼 캐시에서 얻을 수 있다면, 최근 것이 아니고 새로운 버퍼일 경우 파일 시스템은 디바이스 드라이버에게 해당하는 데이터 블럭을 디스크에 요청해서 읽어옴
    • bdflush 커널 데몬  버퍼 캐시를 사용하는 블럭 장치들 사이에 공평하게 캐시 엔트리를 할당하는 데몬
      • 시스템에 있는 더티 버퍼의 개수가 충분히 많아지기를 기다리며 잠들어 있다가 버퍼가 할당되거나 버려질 때 시스템에 있는 더티 버퍼의 개수를 검사
      • 전체 버퍼의 개수 중 더티 버퍼의 비율이 너무 커지면 bdflush 가 깨어나 BUF_DIRTY_LRU 리스트에 데이터 버퍼를 연결 (비율은 기본값이 60%이지만, 시스템에서 버퍼가 필요하다면 이 데몬은 언제든 깨어남)
      • bdflush는 이중에서 적당한 개수(디폴트 500)를 디스크에 기록
    • update 명령어 실행될 때마다 시스템에 있는 모든 더티 버퍼에서 시간이 만료된 것들만 디스크에 기록. 더티 버퍼가 다 쓰여지면 시스템 시간을 표시

버퍼 캐시 커널 자료구조

/proc 파일 시스템

  • 실제로 존재하는 것이 아니며, 내부 파일과 디렉토리를 커널에 있는 정보를 가지고 생성
  • 실제 파일 시스템과 마찬가지로 자신을 VFS에 등록하고 파일이나 디렉토리를 열면 VFS가 inode를 요청
  • 커널의 /proc/devices 파일은 장치들을 나타내는 커널 자료구조로부터 생성됨
  • /proc 파일 시스템은 사용자에게 커널 내부 작업을 볼 수 있는 뷰(view)를 제공
  • 커널 모듈같은 몇몇 리눅스 서브시스템들은 /proc 파일 시스템에 엔트리를 생성함

장치 특수 파일(Device Special File)

  • 리눅스는 하드웨어 장치들을 특수 파일로 저장하며, /dev/null의 경우 널 장치라 해서 윈도우즈의 휴지통과 같은 역할을 함
  • 장치 파일은 파일 시스템에서 어떤 데이터 영역도 차지하지 않으며, EXT2와 VFS는 모두 장치 파일을 inode의 특수한 유형으로 구현
  • 장치 파일에는 2가지 형태가 존재: 문자 특수 파일, 블럭 특수 파일
  • 문자 장치는 문자 모드로 I/O 작업을 할 수 있고, 블럭 장치는 모든 I/O가 버퍼 캐시를 통하도록 되어 있음
  • 장치 파일로 I/O를 요구하면, 시스템 내에 있는 해당 디바이스 드라이버가 요청을 받음
    • 디바이스 드라이버는 실제하지 않고 SCSI 디바이스 드라이버 같은 어떤 서브시스템을 위한 유사 디바이스 드라이버일 수 있음
  • 커널에서 모든 장치는 각각 kdev_t 구조체로 기술되고, 이는 2바이트 길이로 첫번째 바이트는 마이너 장치 번호, 두번째 바이트는 메이저 장치 번호를 의미. (장치의 유형을 구별하는 메이저 번호와, 장치 유형의 하나의 케이스를 구별하는 마이너 번호로 장치 파일이 참조됨)

 

본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.


하드 디스크

https://m.blog.naver.com/PostView.naver?isHttpsRedirect=true&blogId=capemay&logNo=220221154613

  • 하드 디스크는 작동기(actuator)로 원반 표면 위에서 헤드를 움직이게 하며, 이 원반(platter)은 가운데 축(spindle)에 연결되어 일정한 속도로 회전하는데 이 원반은 하나 이상 존재
    • 회전 속도는 3000RPM ~ 10000RPM 사이 다양함
  • 읽기/쓰기 헤드(read/write head)는 원반 표면에 있는 미세한 알갱이에 자성을 띄워서 디스크에 자료를 기록하고, 이 자성을 감지하여 자료를 읽음
  • 각 원반의 윗면과 아랫면에 각각 헤드가 존재하고, 읽기/쓰기 헤드는 물리적으로 원반 표면을 건드리지 않고 아주 얕게 원반 위에 떠있으며, 모든 읽기/쓰기 헤드는 원반 표면에서 동일하게 움직임
  • 원반의 각 표면은 트랙(track)이라는 작은 동심원으로 나누어지며, 바깥쪽에 있을 수록 작은 번호, 중심에 가까울수록 큰 번호의 트랙
  • 실린더(cylinder)는 동일한 번호를 가진 트랙의 집합으로, 원반 양면의 k번 트랙은 모두 k번 실린더
  • 각 트랙은 섹터(sector)로 나뉘며, 섹터는 자료를 디스크에 저장하고 읽어들이는 최소 단위로, 디스크 블럭 크기와 동일 (보통 512바이트로, 이 크기는 디스크 제작 후 포맷 시 지정됨)
    • 터미널에서 출력했을 때 보여지는 디스크 용량이 더 작은 이유는 섹터 중 일부는 디스크 파티션 정보를 저장하므로 제외되기 때문
  • 디스크는 보통 기하학 구조로 표현되며, 부팅 시 IDE 디스크는 다음과 같이 나타낼 수 있음
    • hdb: Conner Peripherals 540MB-CFS540A, 516MB w/64kB Cache, CHS=1050/16/63
    • 디스크 1050개의 실린더(트랙), 16개의 헤드(8개의 원반), 각 트랙에는 63개의 섹터가 있음을 의미
    • 디스크 저장 용량은 516MB
  • 어떤 디스크들은 자동으로 배드 섹터(bad sector)를 찾아내서 디스크가 제대로 작동하도록 인덱스를 다시 부여하기도 함
  • 하드 디스크는 특별한 목적을 위해 할당된 섹터들의 그룹인 파티션(partition) 단위로 쪼개질 수 있고, 파티셔닝은 디스크를 여러 OS에게 나눠주는 등의 이유로 사용
  • 많은 리눅스 시스템은 하나의 디스크에 3개의 파티션을 포함: DOS 파일 시스템, EXT2 파일 시스템, 스왑 파티션
    • 이러한 파티션 정보는 파티션 테이블에 나와 있으며, 이 테이블의 각 엔트리는 파티션의 시작과 끝에 해당되는 헤드와 섹터, 실린더 번호를 포함
    • fdisk로 DOS로 포맷된 디스크는 4개의 1차 디스크 파티션(primary disk partition)을 가질 수 있고, 파티션 테이블의 4개 엔트리 모두 쓰일 필요는 없음
    • fdisk는 3가지 유형의 파티션을 지원: 1차(primary), 확장(ㄷxtended), 논리(logical) 파티션
    • 확장 파티션은 진짜 파티션이 아니며, 여러 논리 파티션을 가지고 있는 것
    • 다음은 2개의 1차 파티션을 갖는 디스크에 fdisk를 실행한 결과: 첫번째 파티션이 헤드 1, 섹터 1, 실린더 0에서 시작하며, 헤드 63, 섹터 32, 실린더 477 까지 있음을 의미. 두번째 파티션은 스왑 파티션으로 다음 실린더 478에서 시작하여 디스크 가장 안쪽 실린더까지 포함 (원반 표면의 바깥에서 안쪽 방향으로 각 파티션들이 위치함)

fdisk 결과

  • 리눅스는 초기화할 때 하드 디스크의 배치도를 메모리에 매핑하는데, 먼저 시스템에 디스크 개수와 각 디스크의 종류를 파악하여 각 디스크 파티션이 나누어지는 구조를 탐색
  • IDE 같은 개별 디스크 서브시스템은 초기화할 때 찾은 디스크를 gendisk 구조체로 기술하며, gendisk_head 포인터가 이 구조체들의 리스트를 참조
    • 각 gendisk 구조체는 블럭 특수 장치의 메이저 번호와 일치하는 고유한 번호를 가짐
    • 아래 그림의 맨 앞 gendisk는 SCSI 디스크 서브시스템의 것이고, 다음은 IDE 디스크 서브시스템의 것으로, 첫번째 IDE 컨트롤러는 "ide0"
    • gendisk 구조체는 리눅스가 파티션을 검사할 때만 쓰이며, 각 디스크 서브시스템은 장치의 메이저와 마이너 장치 번호를 물리 디스크에 있는 파티션과 매핑시킬 수 있도록 각자의 자료구조를 구축함
    • 블럭 장치가 버퍼 캐시나 파일 연산을 통해 읽혀지거나 쓰일 때, 커널은 이 연산을 블럭 장치 특수 파일(e.g., /dev/sda2)에서 발견한 메이저 장치 번호를 이용하여 올바른 장치로 보냄
    • 마이너 장치 번호를 물리 장치에 연결하는 것은 개별 디스크 드라이버나 서브시스템의 역할 (마이너 장치 번호로 장치를 구별)

디스크 커널 자료구조
https://www.twblogs.net/a/5ca59651bd9eee5b1a0720f4

  • IDE(Intergrated Disk Electronics) 디스크
    • IDE는 SCSI 같은 I/O 버스가 아닌 디스크 인터페이스
    • 각 IDE 컨트롤러는 2개의 디스크까지 지원: 주 디스크(master), 종속 디스크(slave)
    • IDE는 1초에 3.3 Mbytes의 데이터를 전송, 디스크 최대 크기는 538MB
    • 확장 IDE(Extended IDE, EIDE)는 디스크 크기를 최대 8.6GB로 가지며, 전송 속도를 초당 16.6MB로 올린 것
    • IDE/EIDE 디스크는 SCSI 디스크보다 저렴하며 대부분 PC는 IDE 컨트롤러를 포함
    • 리눅스는 IDE 디스크의 이름을 컨트롤러(최대 2개)를 발견한 순서에 따라 부여함
      • 1차 IDE 컨트롤러의 주 디스크는 /dev/hda 이고 종속 디스크는 /dev/hdb
      • 2차 IDE 컨트롤러의 주 디스크는 /dev/hdc
    • IDE 서브시스템은 커널에 IDE 컨트롤러를 등록하지만 디스크를 등록하지는 않음
    • 1차 IDE 컨트롤러의 메이저 장치 번호는 3이고, 2차 IDE 컨트롤러의 것은 22일 때, blk_dev와 blkdevs 배열의 3번과 22번 인덱스에 IDE 서브시스템 엔트리가 저장됨
    • 커널은 블럭 특수 파일을 관리하는 IDE 서브시스템에 대한 파일 연산이나 버퍼 캐시 연산을 메이저 장치 번호를 인덱스로 사용하여 알아낸 IDE 서브시스템으로 전달하며, 어떤 디스크에 대한 요청인지는 IDE 서브시스템이 마이너 장치 번호를 사용하여 판별
    • IDE 서브시스템 초기화
      • 커널은 시스템의 CMOS 메모리에 디스크 정보를 살펴보고, 발견한 디스크의 기하학 정보를 BIOS로부터 알아내 이 정보를 각자의 ide_hwif_t 구조체를 설정하는데 사용함
      • 최근 PCI EIDE 컨트롤러를 포함하는 경우 PCI 칩셋을 사용하는데, 여기서 PCI BIOS 콜백을 이용해서 컨트롤러를 찾음
      • IDE 컨트롤러가 발견되면, 연결된 디스크를 반영하여 ide_hwif_t가 설정됨
      • IDE 드라이버가 I/O 메모리 공간에 있는 IDE 명령 레지스터에 쓰면서 동작하는데, 각 컨트롤러는 리눅스 블럭 버퍼 캐시와 가상 파일 시스템에 자신을 등록 (blk_dev와 blkdevs 배열에 추가되는 것)
      • 추가로, 인터럽트에 대한 제어권을 요청하고, 부팅 시 발견된 컨트롤러마다 gnedisk 구조체를 생성해 gendisk_head 포인터가 참조하는 리스트에 추가 (이 리스트는 디스크의 파티션을 찾는데 사용됨)
  • SCSI(Small Computer System Interface) 디스크
    • SCSI 버스 하나 이상의 호스트를 포함하여 버스마다 8개까지의 장치를 지원할 수 있는 1:1 데이터 버스
    • 각 장치는 고유 식별자를 가지며, 대개 디스크에 있는 점퍼로 설정됨
    • 버스에 있는 두 장치 사이에는 동기적 또는 비동기적 데이터 전송이 가능하며, 32비트 크기로 초당 40MB까지 허용
    • SCSI 버스는 데이터 뿐만 아니라 상태 정보도 전송
    • 전송 시작(initator)과 전송 대상(target) 사이의 하나의 트랜잭션은 8개의 서로 다른 상태를 가지며, SCSI 버스의 현재 상태는 5개의 신호로부터 알 수 있음
      • BUS FREE 버스에 대한 제어권을 가진 장치 또는 트랜잭션이 없음
      • ARBITRATION 어떤 장치가 자신의 식별자를 보내 SCSI 버스에 대한 제어권을 얻는 중(중재), 동시에 발생할 경우 더 높은 번호에 제어권이 주어짐
      • SELECTION 장치가 중재를 통해 제어권을 얻으면, SCSI 요청을 받을 대상에게 명령을 보낼 준비라는 신호를 전달
      • COMMAND 6/10/12 바이트의 명령을 전송 시작자에서 전송 대상으로 전달
      • DATA IN, DATA OUT 데이터가 전달되는 상태
      • STATUS 모든 명령을 완료한 상태로, 성공과 실패를 나타내는 바이트를 응답해야 함
      • MESSAGE IN, MESSAGE OUT 추가적인 정보 전달
    • SCSI 서브시스템의 2가지 요소(호스트와 장치)는 각 자료구조로 표현됨
    • 호스트(host) SCSI 컨트롤러, 같은 종류의 컨트롤러가 하나 이상 존재하면 각각 별도의 호스트로 표현
      • SCSI 디바이스 드라이버는 컨트롤러가 여러개 일 때 제어 가능
      • SCSI 호스트는 대부분 SCSI 명령의 전송 시작자
    • 장치(device) 가장 일반적인 장치 유형으로 SCSI 디스크가 있으며, 테이프나 CD-ROM 같은 여러 종류를 지원하기도 함.
      • 각 장치는 서로 다르게 취급되며, SCSI 장치는 대부분 SCSI 명령의 전송 대상

SCSI 디스크 커널 자료구조

  • SCSI 서브시스템 초기화
    • 먼저 시스템에 있는 SCSI 컨트롤러(호스트)를 찾은 뒤, SCSI 버스를 검사하여 모든 장치를 탐색
    • 각 장치를 초기화: 일반 파일 연산과 버퍼 캐시 블럭 장치 연산을 매핑하여 나중에 사용될 수 있게 함
    • 커널을 빌드할 때 SCSI 호스트 어댑터(컨트롤러) 중 제어할 하드웨어의 것을 찾음
    • 커널에 포함된 호스트들은 builtin_scsi_hosts 배열에 Scsi_Host_Template 엔트리를 가지며, 이 구조체는 각 SCSI 호스트에 어떤 SCSI 장치가 연결되었는지 알기 위해 호스트마다 고유 루틴에 대한 포인터를 포함
    • SCSI 서브시스템은 자신을 설정하는 동안 이 루틴들을 호출하는데 루틴은 각 호스트에 대한 SCSI 디바이스 드라이버의 일부
    • 실제 장치가 연결된 호스트는 자신의 Scsi_Host_Template 구조체를 활성화된 호스트 목록인 scsi_hosts 리스트에 추가
    • 감지된 호스트 유형에 해당하는 호스트는 scsi_hostlist 리스트에 있는 Scsi_Host 구조체로 기술됨
    • 모든 호스트를 발견하면, SCSI 버스에 있는 장치로 TEST_UNIT_READY 명령을 전달해 버스에 연결된 장치들을 탐색
    • 장치가 응답하면 ENQUIRY 명령을 보내 신원 확인을 통해 커널에 제작자 이름과 장치 모델명 및 개정 이름을 전달
    • SCSI 명령은 Scsi_Cmnd 구조체로 기술되며, Scsi_Host_Template 구조체 있는 디바이스 드라이버 루틴을 호출할 때 Scsi_Cmnd 구조체가 전달됨
    • SCSI 장치는 Scsi_Device 구조체로 기술되며, 각각 부모 Scsi_Host 구조체를 참조하는데, 모든 Scsi_Device 구조체는 scsi_devices 리스트에 추가됨
    • 장치의 유형은 4가지가 존재: 디스크, 테이프, CD, 일반
      • 메이저 블럭 장치 유형으로 커널에 별도로 등록
      • 각 유형마다 장치 테이블을 관리하며, 이 테이블은 커널의 블럭 연산(파일, 버퍼 캐시)을 올바른 디바이스 드라이버나 SCSI 호스트로 보내는데 사용됨
      • 유형별로 Scsi_Device_Template 구조체를 생성하며, 이는 장치에 대한 정보 및 다양한 작업을 수행하는 루틴들의 주소를 포함. SCSI 서브시스템은 이 구조체를 이용해 장치의 유형별 루틴을 호출
      • 어떤 유형을 갖는 SCSI 장치가 하나 이상 발견되면 Scsi_Type_Template 구조체가 scsi_Devicelist 리스트에 추가됨
      • 초기화의 마지막 상태는 등록된 각 Scsi_Device_Template 구조체로부터 종료 함수를 부르는 것으로, SCSI 디스크 유형의 경우 발견한 모든 디스크를 회전시켜 각 디스크 구조를 기록한 뒤, 모든 SCSI 디스크를 나타내는 gendisk 구조체를 디스크 구조체 리스트에 추가
  • 블럭 장치 요청 전달
    • SCSI 서브시스템을 초기화하면 SCSI 장치들을 사용할 수 있으며, 정상 동작하는 장치 유형은 커널에 자신을 등록하여 블럭 장치 요청이 들어올 때마다 해당 장치 유형으로 전달되게 함
    • 요청은 blk_dev 배열을 인덱싱하여 버퍼 캐시 요청이나 blkdevs 배열을 인덱싱하여 파일 연산 요청으로 나뉨
    • SCSI 디스크에서 하나 이상의 EXT2 파일 시스템 파티션을 가지는 경우, 디바이스 드라이버는 파티션 중 하나를 마운트할 때 커널 버퍼 요청을 올바른 파일 시스템으로 전달
    • SCSI 디스크 파티션에서 한 블럭의 데이터를 읽고 쓰는 요청은 SCSI 디스크의 current_request 리스트에 새로운 request 구조체를 추가하여 처리됨. 처리중이면 버퍼 캐시는 다른 일을 할 필요가 없으며, 처리중이 아니면, 계속 요청 큐를 하나씩 처리
      • SCSI 디스크 파티션의 마이너 장치 번호 중 일부를 사용하여 blk_dev 배열을 인덱싱하며, 얻은 자료구조의 일부에 current_request 포인터가 존재
      • 각 Scsi_Disk 구조체는 이 장치를 나타내는 Scsi_Device 구조체에 대한 포인터를 가짐
      • Scsi_Device 구조체는 각자 소유한 Scsi_Host 구조체를 순서대로 참조
    • 버퍼 캐시로부터 온 request 구조체는 Scsi_Cmnd 구조체로 변환되고, 이 구조체는 Scsi_Host 구조체의 큐에 추가됨
    • 한 번 데이터 블럭을 읽거나 쓰고 나면, 이 요청들은 개별 SCSI 디바이스 드라이버에 의해 처리됨

네트워크 장치

  • 네트워크 디바이스 드라이버는 커널이 부팅하면서 초기화하는 동안 제어하는 장치를 커널에 등록하는데, 네트워크 장치는 device 구조체로 표현되며, 각 구조체는 장치 정보 및 리눅스에서 네트워크 프로토콜들이 장치의 서비스를 이용할 수 있는 (대부분 장치를 통한 데이터 전송과 관련된) 함수들의 주소를 포함
  • 네트워크 장치 특수 파일은 초기화 과정에서 차례로 생성되며, 이들 이름은 장치 유형을 나타내는 표준 이름이며 0부터 시작하는 번호가 뒤에 붙어서 식별됨
    • 이더넷 장치: /dev/eth0, /dev/eth1, /dev/eth2
    • SLIP 장치: /dev/sl0, /dev/sl1, /dev/sl2
    • PPP 장치: /dev/ppp0, /dev/ppp1, /dev/ppp2
    • 루프백 장치: /dev/lo
  • device 구조체가 포함한 장치 정보 등 모든 정보는 부팅 시 초기화될 때 설정됨
    • 버스 정보 디바이스 드라이버가 장치를 제어하기 위한 것
    • IRQ 번호 장치가 사용하는 인터럽트 번호
    • 베이스 주소 장치의 제어 레지스터 및 상태 레지스터가 있는 I/O 공간의 주소
    • DMA 채널 네트워크 장치가 사용하는 DMA 채널 번호
    • 인터페이스 플래그 네트워크 장치의 특징과 능력을 설명

인터페이스 플래그

  • 프로토콜 정보 네트워크 프로토콜 게층이 장치를 제어하는 방법을 나타냄
    • MTU 링크 계층에서 붙이는 헤더를 제외하고 전송 가능한 최대 패킷 크기
    • Family 장치가 지원할 수 있는 프로토콜 계열 e.g., AF_INET: 인터넷 주소
    • Type 하드웨어 인터페이스 유형으로, 장치에 연결된 매체 e.g., 이더넷
    • Address device 구조체는 IP 주소를 포함하여 여러 주소를 가짐
  • 패킷 큐(Packet Queue) 네트워크 장치가 전송하기를 기다리는 sk_buff 구조체의 리스트
    • 보내고 받는 모든 네트워크 데이터(패킷)은 sk_buff 구조체로 기술됨
  • 각 장치는 프로토콜 계층에서 호출 가능한 표준 함수 집합을 제공하여 전송받은 데이터를 올바른 프로토콜 계층으로 전달
  • 이는 셋업하고 프레임을 전송하는 루틴 외에 표준 프레임 헤더를 추가하고 통계 정보를 모으는 루틴도 포함되어 있으며, 통계 정보는 ifconfig 명령으로 출력 가능
  • 네트워크 장치 초기화
    • 네트워크 게층은 장치에 고유한 작업을 수행할 때 device 구조체에 있는 서비스 루틴을 호출
    • 단, device 구조체는 최초에 초기화나 장치를 탐사(probe)하는 루틴의 주소만 가짐
    • 장치의 초기화 루틴을 호출하면, 구동할 컨트롤러가 존재하는지 나타내는 상태값을 얻게 되고, 아무런 장치도 못찾으면 dev_base 포인터가 참조하는 device 리스트에 있는 엔트리가 제거됨
    • 장치를 찾게 되면, device 구조체의 나머지 부분을 장치 정보 및 드라이버가 지원하는 함수들로 설정
    • 장치 리스트에는 eth0 부터 eht7 까지 8개의 표준 엔트리가 있는데, 초기화 코드는 장치를 찾을 때까지 이더넷 디바이스 드라이버를 하나씩 시도
    • 이더넷 장치를 찾으면 device 구조체의 내용을 채우고, 제어할 하드웨어를 초기화한 뒤 사용할 IRQ 번호 및 DMA 채널 등을 알아냄. 8개 모두 할당되면 이더넷 장치를 더 이상 찾지 않음
  • 네트워크 장치 파일은 실제로 장치가 존재하는 경우에만 생성되나, 보통 문자 장치나 블럭 장치는 실제로 장치가 존재하지 않더라도 장치 특수 파일이 존재함

 

본 글은 The Linux Kernel 을 정리한 것이며, 출처를 밝히지 않은 모든 이미지는 원글에 속한 것입니다.


하드웨어 장치의 추상화

  • 운영체제는 하드웨어 장치별로 다른 특징을 보이지 않는, 일관된 인터페이스를 사용자에게 제공
  • 모든 물리 장치는 각자의 하드웨어 컨트롤러를 가지며, 모든 하드웨어 컨트롤러는 각자의 고유한 제어/상태 레지스터(Control and Status Registers, CSRs)를 포함
    • 키보드, 마우스, 직렬포트는 Super I/O 칩이 제어하며, IDE 디스크는 IDE 컨트롤러, SCSI 디스크는 SCSI 컨트롤러가 제어
    • CSRs는 장치를 시작 및 중단하고 초기화하며, 문제가 발생했을 때 진단하는 용도로 사용됨
  • 커널에는 하드웨어 컨트롤러를 제어하고 관리하기 위해 디바이스 드라이버(Device Driver)가 존재하는데, 이는 커널 모드에서 실행되고 메모리에 존재하며 저수준 하드웨어 처리 루틴을 포함한 공유 라이브러리
    • 각 디바이스 드라이버는 각자 관리하는 장치들의 특성들을 처리
  • 모든 물리 장치들은 일반 파일처럼 사용자에게 보여지며, 이를 장치 특수 파일(device special file)이라고 하는데, 파일을 다루는 표준 시스템 콜을 이용해서 열고, 닫고, 읽고, 쓸 수 있음
    • IDE 디스크는 /dev/hda 로 나타냄
    • 네트워크 장치들도 파일로 표시되지만 커널이 네트워크 컨트롤러를 초기화할 때 장치 특수 파일이 생성됨
    • 동일한 디바이스 드라이버로 제어되는 모든 장치는 동일한 메이저 장치 번호를 가지며, 각 장치나 컨트롤러를 구분하기 위해 마이너 장치 번호를 사용
  • 리눅스는 문자, 블럭, 네트워크 3가지 종류로 하드웨어 장치의 추상화를 제공
    • 문자 장치는 버퍼 없이 하나씩 바로 읽고 쓸 수 있음 e.g., 키보드
    • 블럭 장치는 일정한 크기(512 또는 1024바이트)의 배수로만 읽고 쓸 수 있음 e.g., 하드 디스크
    • 네트워크 장치는 BSD 소켓 인터페이스로 하위 네트워크 계층(TCP/IP)에 접근할 수 있음 e.g., 이더넷
  • 커널 디바이스 드라이버의 공통 특성
    • 커널 코드의 일부로, 잘못되면 시스템에 큰 피해를 줄 수 있음
    • 커널이나 자신이 속한 서브시스템에 표준 인터페이스를 제공
      • 터미널 디바이스 드라이버는 커널에 파일 I/O 인터페이스를 제공
      • SCSI 디바이스 드라이버는 커널에 파일 I/O와 버퍼 캐시 인터페이스를 제공
    • 커널 메커니즘 및 서비스 제공: 메모리 할당, 인터럽트 전달, 대기 큐 같은 커널 서비스 사용
    • 대부분 디바이스 드라이버는 필요 시 물리 메모리에 로드하고 필요없으면 언로드 가능
    • 커널에 포함된 상태로 함께 컴파일 될 수 있고, 장치는 컴파일 시 설정 가능
    • 부팅 후 디바이스 드라이버가 초기화 될 때, 시스템은 제어할 하드웨어 장치를 가지며, 없다하더라도 문제가 되지 않음

폴링(Polling)과 인터럽트(Interrupt)

  • 장치에 명려을 전달할 때, 그 명령이 언제 끝났는지 알 수 있는 방법
  • 폴링하는 디바이스 드라이버는 시스템 타이머를 이용하여 어느정도 시간이 지나면 커널이 디바이스 드라이버에 있는 한 루틴을 호출. 이 타이머 루틴은 명령이 수행되었는지 상태를 검사 (주기적으로 확인하는 작업)
  • 인터럽트를 이용하는 디바이스 드라이버는 제어하는 장치가 작업을 끝냈거나 서비스를 받아야할 때 인터럽트를 발생시킴. 커널은 이 인터럽트를 올바른 디바이스 드라이버로 전달함 (비동기적으로 요청하고 결과를 받음)
    • 이더넷 디바이스 드라이버는 네트워크에서 패킷을 받을 때마다 인터럽트를 발생시킴
  • 디바이스 드라이버는 인터럽트를 사용하기 위해 인터럽트 처리 루틴의 주소와 사용할 인터럽트 번호를 커널에 등록
    • /proc/interrupts 파일을 살펴보면, 디바이스 드라이버가 사용중인 인터럽트를 알 수 있음
    • 드라이버가 초기화될 때 인터럽트 자원(제어권)을 커널에 요청

/proc/interrupts, 중간 숫자가 인터럽트 번호

  • 시스템의 어떤 인터럽트들은 처음부터 고정되어 있으며, 플로피 디스크 컨트롤러의 경우 항상 6번을 사용. 또는 PCI 장치에서 발생하는 인터럽트들은 부팅 시 동적으로 할당됨
  • PCI 디바이스 드라이버는 제어할 장치의 인터럽트 번호(IRQ)를 먼저 알아낸 뒤(즉, 탐사 과정) 인터럽트의 제어권을 요청
    • 리눅스는 표준 PCI BIOS 콜백을 지원해서 장치 정보를 제공
  • 인터럽트가 PIC에서 CPU로 전달될 때는 인터럽트 모드에서 처리 루틴을 실행하는데, 다른 인터럽트가 발생하지 못하므로 되도록 처리 루틴에서는 적은 일을 하고 인터럽트 전으로 돌아갈 수 있도록 스택에 컨텍스트를 저장하고 복구함
    • 작업량이 많으면 나중에 해도 될 일을 커널의 하반부 핸들러나 작업큐에 넣어 처리 (인터럽트 모드를 빨리 벗어나려는 의도)

직접 메모리 접근(Direct Memory Access, DMA)

  • 인터럽트를 사용하는 디바이스 드라이버는 데이터의 양이 작을 때는 잘 동작하지만, 디스크 컨트롤러 및 이더넷 장치 같은 데이터 전송률이 높은 장치의 경우 CPU 이용률이 높을 수 밖에 없음
  • DMA 컨트롤러는 장치와 시스템 메모리 사이에 CPU 도움 없이 데이터 전송을 가능하게 함
  • ISA DMA 컨트롤러는 8개의 DMA 채널을 제공하고, 이 중 7개를 디바이스 드라이버가 사용하는데, 드라이버끼리는 채널을 공유할 수 없음
    • 각 DMA 채널은 16비트 주소 레지스터와 16비트 카운터 레지스터로 접근 가능
    • 데이터 전송을 초기화하기 위해, 디바이스 드라이버는 DMA 채널의 두 레지스터와 데이터 전송 방향(읽기/쓰기)을 함께 설정
  • DMA를 사용해도 좋다는 명령을 보내야 사용 가능하며, 데이터 전송이 완료되면 장치는 인터럽트를 발생시키고, 전송하는 동안 CPU는 다른 일을 할 수 있음
  • DMA 컨트롤러는 물리 메모리에 직접 접근하기 때문에, 프로세스의 가상 메모리로 DMA를 바로 사용할 수 없고, DMA에서 사용하는 메모리는 물리 메모리에서 연속된 블럭으로 할당되어야 함
  • 사용자는 시스템 콜로 프로세스가 사용하는 물리 페이지에 락을 걸어 DMA 작업 중 스왑 아웃되는 것을 방지할 수 있음
  • DMA가 사용하는 메모리는 하부 16MB 로 제한되어 있어 DMA 컨트롤러는 물리 메모리 전체에 접근 불가
  • 인터럽트처럼 어떤 장치가 사용하는 DMA 채널이 고정되어 있고, 이더넷 장치의 경우 하드웨어에 점퍼로 설정 가능
    • 또는 장치들이 CSRs를 통해 사용할 DMA 채널을 알려주고 디바이스 드라이버가 빈 채널을 사용할 수도 있음
  • 리눅스는 DMA 채널 하나당 dma_chan 구조체를 생성하며, 이 구조체의 배열을 이용하여 채널의 사용유무를 추적
    • 이 구조체는 2개의 항목을 포함: DMA 채널의 소유자를 나타내는 문자열 및 해당 채널의 할당 유무를 나타내는 플래그
    • cat /proc/dma 명령을 실행하면 나오는 것이 dma_chan 구조체의 배열
  • DMA가 없을 때와 있을 때의 차이
    • DMA 없을 때, CPU가 장치로부터 데이터 읽고 쓰는 작업을 반복해야 되서 CPU 이용률이 증가함
    • DMA 있을 때, CPU는 장치에게 DMA 시작 신호 및 DMA 완료 인터럽트만 수신하고, 데이터 전송에는 개입하지 않기 때문에 CPU 이용률이 낮음

http://jake.dothome.co.kr/dma-1/

디바이스 드라이버가 제공하는 인터페이스

  • 디바이스 드라이버의 메모리
    • 디바이스 드라이버는 커널 코드의 일부이기 때문에 물리 메모리만 사용 가능
    • 인터럽트를 받았거나 하반부 핸들러 또는 작업 큐 핸들러가 스케줄되었을 때, 현재 프로세스는 바뀔 수 있는데, 이는 디바이스 드라이버가 특정 프로세스가 실행될 때 독립적으로 다른 한 켠(백그라운드)에서 실행되기 때문 (커널 코드의 특성)
    • 부팅 시 커널에서 사용할 메모리가 할당되고 메모리를 해제하는 루틴을 전달받아, 디바이스 드라이버는 사용할 메모리를 2의 제곱승 단위로 할당 받거나 해제할 수 있음
    • 디바이스 드라이버가 할당 받은 메모리를 DMA로 입출력하려면, 그 메모리를 DMA 가능이라고 지정해야 함
  • 커널이 모든 종류의 디바이스 드라이버에게 서비스를 요청할 때 사용할 수 있는 공통적인 인터페이스가 존재
    • 서로 다른 장치들 또는 디바이스 드라이버들을 일관적이게 다루기 위한 인터페이스
  • 디바이스 드라이버는 장치가 없더라도 시스템이 동작할 수 있도록 커널에 의해 초기화될 때 스스로 커널에 등록
    • 커널은 등록된 디바이스 드라이버들을 테이블로 관리하며, 이 테이블은 해당 종류의 장치와 인터페이스를 제공하는 함수들의 포인터를 저장하고 있음
  • 문자 장치(Character Device)
    • 초기화될 때, 이 장치의 디바이스 드라이버는 device_struct 구조체를 생성하여 chrdevs 배열에 추가하는 것으로 커널에 자신을 등록
    • /proc/devices 에서 문자 장치에 대한 내용은 모두 chrdevs 배열에서 가져온 것
    • 메이저 장치 번호(tty 장치는 4번 같은)는 이 배열의 인덱스로 사용되며, 고정되어 있음
    • device_struct 구조체는 2가지 항목을 포함
      • 디바이스 드라이버의 등록 이름에 대한 문자열 포인터
      • 파일 연산 블럭에 대한 포인터
    • 프로세스가 문자 장치를 나타내는 문자 특수 파일(character specific file)을 열면, 커널은 올바른 문자 디바이스 드라이버의 파일 처리 루틴이 호출되도록 설정함
      • 각 장치 특수 파일은 VFS inode로 기술되며, VFS inode는 장치의 메이저 식별자와 마이너 식별자를 가짐
      • 각 VFS inode는 파일 연산 루틴에 대한 포인터를 가지는데, 생성 시 기본 문자 장치 연산으로 설정됨 (이 연산은 가리키는 파일 시스템 객체에 따라 다름)
      • 파일 연산 함수를 실행하면, 장치의 메이저 식별자로 chrdevs 배열에 인덱싱 --> 이 장치에 대한 파일 연산 블럭을 가져옴 --> file 구조체와 파일 연산 포인터가 디바이스 드라이버의 것을 참조하도록 device_struct 구조체를 설정
      • 이후 프로세스에서 호출하는 모든 파일 연산은 문자 장치의 파일 연산으로 매핑되어 호출됨
  • 블럭 장치(Block Device)
    • 초기화될 때, 이 장치의 디바이스 드라이버는 device_struct 구조체를 생성하여 blkdevs 배열에 추가하는 것으로 커널에 자신을 등록
    • 문자 장치와 마찬가지로, 메이저 장치 번호는 blkdevs 배열의 인덱스로 사용되며, 다른 점은 장치의 유형을 분류하는 클래스가 존재한다는 것. SCSI 장치와 IDE 장치를 클래스로 구분하며, 각 클래스는 커널에 파일 함수를 제공해야 함
    • 각 클래스의 블럭 장치들을 사용하는 디바이스 드라이버는 고유의 클래스 인터페이스를 제공하는데, 예를 들어, SCSI 디바이스 드라이버는 "SCSI 서브시스템이 커널에 파일 함수를 제공하기 위해 사용하는 인터페이스"를 서브시스템에 제공해야 함
    • 모든 블럭 디바이스 드라이버는 파일 연산과 함께 버퍼 캐시에 대한 인터페이스를 제공
    • 버퍼 캐시에 한 블럭의 데이터를 읽고 쓰기 위한 요청을 보내면, 각 드라이버는 all_requests (정적 리스트)에서 request 구조체를 할당받아 blk_dev_struct 구조체에 있는 request 리스크 (요청 큐)에 할당받은 것을 추가. 이후 요청 큐를 처리하기 위해 blk_dev_struct 구조체에 있는 요청 루틴의 주소로 점프해서 요청을 처리
      • blk_dev_struct 구조체는 blk_dev 배열의 원소이며, 메이저 장치 번호로 인덱싱됨
      • 각 request 구조체는 하나 이상의 buffer_head 구조체에 대한 포인터를 가지며, 이 구조체는 버퍼 캐시에 의해 락됨
      • 이 버퍼로 블럭 연산이 끝날 때까지 프로세스는 대기 상태가 됨
    • 요청이 처리되면 드라이버는 request 구조체에서 각 buffer_head 구조체를 제거하고 갱신되었음을 표시한 뒤 락을 해제. 대기 중인 프로세스는 다음 스케줄링에서 실행됨

blk_dev 구조체
https://lwn.net/Kernel/LDD2/ch12.lwn
https://lwn.net/Kernel/LDD2/ch12.lwn

 

+ Recent posts