멀틱스 운영체제 이후 유닉스 운영체제가 개발되었고, 멀틱스의 철학이 승계되었다. 다음은 두 운영체제에 대한 비교이다.
멀틱스의 혁신을 단순화한 결과가 유닉스이다. 유닉스의 첫 번째 버전은 순수 어셈블리어만으로 작성되었고, 1973년에 최초로 C를 사용한 유닉스 버전 4가 출시되었다.
BCPL은 컴파일러를 작성할 목적으로 고안된 프로그래밍 언어이다. C 언어 전 있었던 B 언어는 자료형이 없는(typeless) 언어였기 때문에 바이트가 아닌 워드 단위로만 작업이 가능해 시스템 프로그래밍에 불리했다. 따라서 NB(New B) 언어 개발 시까지 B 언어는 계속 수정되었고 이로부터 구조체가 유래되었다.
다음은 B의 단점으로, C 개발의 계기가 되었다.
워드 기반으로 설계된 B 언어는 New B로 수정되었으며, 이것이 C라고 불린다.

링 구조의 가장 안쪽에는 하드웨어(hardware)가 존재한다. 이를 커널(kernel)이 감싸고 있으며, 하드웨어에 대한 래퍼(wrapper)의 역할을 한다. 커널은 시스템의 전체 자원을 이용할 수 있는 가장 높은 권한을 가진다.
셸(shell)은 사용자 응용프로그램과 커널 간의 인터페이스 역할을 한다. 이는 여러 작은 프로그램으로 구성되어 커널에 대한 서비스를 제공한다. 라이브러리 모음도 포함하며, 모두 C로 작성되어 있다. 단일 유닉스 규격(SUS: Simple Unix Specification)에 있는 라이브러리를 기반으로 표준 인터페이스를 반드시 노출해야 한다.
사용자 응용프로그램은 유닉스 철학의 이식성의 원리 때문에 시스템 호출(system call)을 통해 커널에 직접 접근하기보단 셸 링이 제공하는 API와 도구를 사용해야 한다.
바깥 링이 서비스를 사용할 수 있도록 내부 링은 인터페이스를 제공해야 한다.
모든 유닉스 시스템은 셸 링에서 같은 응용프로그램 프로그래밍 인터페이스(API: Application Programming Interface)를 노출한다. 표준 인터페이스만을 사용하는 C 소스 코드는 모든 유닉스 시스템에서 빌드 및 실행할 수 있다.
유닉스 표준과 완전히 호환 가능하도록 만들어진 시스템을 유닉스 호환 시스템이라 하며, 대표적인 예로 BSD가 있다. 반면 유닉스 표준을 부분적으로 따르는 시스템은 유닉스 계열 시스템이라 하며, 대표적인 예로 리눅스가 있다.
셸 링이 제공하는 다양한 API는 SUS 표준 아래 수집된다. 다음은 SUS v4에 있는 API의 목록이다.
SUS는 시스템에서 헤더를 어디에서 사용할 수 있고, 어디에 위치해 있는지를 설명할 뿐이다. 표준에 따르면 이는 /usr/include 또는 /usr/local/include에 있어야 한다.
만약 시스템 인터페이스와 헤더 인터페이스를 각 유닉스 버전(Unix flavor)마다 상이한 구현과 합치면 C 표준 라이브러리 또는 libc가 된다. 유닉스 시스템에서 개발되는 모든 C 프로그램은 더 하위에 있는 커널이나 하드웨어와 통신하기 위해 libc를 사용한다.
유닉스 호환 시스템은 SUS 표준을 전적으로 준수하지만 유닉스 계열 시스템은 부분적으로만 준수한다. 그러므로 이론적으로 유닉스 호환 시스템을 위해 개발된 프로그램은 다른 유닉스 호환 시스템에 이식할 수 있어야 하지만, 유닉스 계열 시스템에는 이식되지 않을 수도 있다.
리눅스 탄생 이후 여러 유닉스 계열 운영체제가 개발되었고, 이는 SUS 표준의 부분집합에 특정 이름을 부여하는 토대가 되었다. 이것들을 이식 가능한 운영체제 인터페이스(POSIX: Portable Operating System Interface)라고 한다. POSIX 호환성은 표준 셸 링에 대한 것이고, 커널에 대한 것은 아니다.
SUS와 POSIX 표준은 모두 인터페이스에 대한 규칙이다. 각 유닉스 시스템은 이에 대한 자체적인 구현이 있고, 셸 링에 속하는 libc 라이브러리에 위치한다. 즉, 유닉스 시스템에서 셸 링은 표준 방식으로 제공해야 하는 libc 구현을 포함하여 요청을 커널 링이 처리하도록 전달한다.
사용자 응용프로그램은 셸 루틴을 실행하기 위해 libc 라이브러리와 연결되거나 기존 유틸리티 프로그램을 실행할 수 있어야 한다. 기존 유틸리티 프로그램도 자체적으로 libc 라이브러리를 사용한다. 그리고 유닉스 계열의 운영체제를 설계할 수 있으려면 컴파일 파이프라인과 링크 메커니즘이 필요하다. 이때 C의 모든 특성이 기여한다.
libc 또는 셸 링의 함수는 커널 기능을 사용하기 위해 시스템 호출(system call) 메커니즘을 사용한다.
코드 박스 10-1 [예제 10-1] 셸 링에 포함된 sleep 함수를 호출
#include <unistd.h>
int main(int argc, char** argv) {
sleep(1);
return 0;
}
unistd.h 헤더 파일, sleep 함수 모두 SUS가 노출한 인터페이스에 속한다.
셀 박스 10-1 [예제 10-1]이 불러온 시스템 호출을 추적하기 위해 strace를 사용하는 [예제 10-1]을 빌드하고 실행하기
$ gcc 10_1.c -lc -o 10_1.out
$ strace ./10_1.out
...
유닉스 계열 시스템에서는 truss, 리눅스 시스템에서는 strace를 사용하여 시스템 호출을 관찰할 수 있다.
셀 박스 10-2 [예제 10-1]에서 불러온 시스템 호출을 보여주는 strace의 출력 결과
$ strace ./10_1.out
execve("./10_1.out", ["./10_1.out"], 0x7fffee3a2010 /* 84 vars */) = 0
brk(NULL) = 0x58669b0e3000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x705110405000
access("/etc/ld.so.preload", R_OK) = -1 ENOENT (그런 파일이나 디렉터리가 없습니다)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC) = 3
fstat(3, {st_mode=S_IFREG|0644, st_size=71195, ...}) = 0
mmap(NULL, 71195, PROT_READ, MAP_PRIVATE, 3, 0) = 0x7051103f3000
close(3) = 0
openat(AT_FDCWD, "/usr/lib/x86_64-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
read(3, "\177ELF\2\1\1\3\0\0\0\0\0\0\0\0\3\0>\0\1\0\0\0 \250\2\0\0\0\0\0"..., 832) = 832
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 840, 64) = 840
fstat(3, {st_mode=S_IFREG|0755, st_size=2186512, ...}) = 0
pread64(3, "\6\0\0\0\4\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0@\0\0\0\0\0\0\0"..., 840, 64) = 840
mmap(NULL, 2231696, PROT_READ, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0x705110000000
mmap(0x705110028000, 1671168, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x28000) = 0x705110028000
mmap(0x7051101c0000, 319488, PROT_READ, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x1c0000) = 0x7051101c0000
mmap(0x70511020e000, 24576, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x20d000) = 0x70511020e000
mmap(0x705110214000, 52624, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0x705110214000
close(3) = 0
mmap(NULL, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x7051103f0000
arch_prctl(ARCH_SET_FS, 0x7051103f0740) = 0
set_tid_address(0x7051103f0d68) = 13190
set_robust_list(0x7051103f0a20, 24) = 0
rseq(0x7051103f0680, 0x21, 0, 0x53053053) = 0
mprotect(0x70511020e000, 16384, PROT_READ) = 0
mprotect(0x586665327000, 4096, PROT_READ) = 0
mprotect(0x70511044a000, 8192, PROT_READ) = 0
prlimit64(0, RLIMIT_STACK, NULL, {rlim_cur=8192*1024, rlim_max=RLIM64_INFINITY}) = 0
getrandom("\x29\x05\x8d\xc1\x49\xe3\xe7\x08", 8, GRND_NONBLOCK) = 8
munmap(0x7051103f3000, 71195) = 0
clock_nanosleep(CLOCK_REALTIME, 0, {tv_sec=1, tv_nsec=0}, 0x7ffe0d602000) = 0
exit_group(0) = ?
+++ exited with 0 +++
sleep 함수 호출이 clock_nanosleep 함수 호출로 변환되었음을 확인할 수 있다.
셀 박스 10-3 nanosleep 시스템 호출에 대해 할당된 매뉴얼 페이지
$ man nanosleep
nanosleep(2) System Calls Manual nanosleep(2)
NAME
nanosleep - high-resolution sleep
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <time.h>
int nanosleep(const struct timespec *duration,
struct timespec *_Nullable rem);
Feature Test Macro Requirements for glibc (see feature_test_macros(7)):
nanosleep():
_POSIX_C_SOURCE >= 199309L
...
다음으로 libc에서 실제로 시스템 호출을 불러오는 지점을 찾아보자.
셀 박스 10-4 FreeBSD 프로젝트를 복제하고 특정 커밋으로 이동하기
$ git clone https://github.com/freebsd/freebsd
...
$ cd freebsd
$ git reset --hard bf78455d496
...
셀 박스 10-5 FreeBSD libc 파일에 있는 nanosleep 시스템 호출 관련 엔트리 찾기
$ cd lib/libc
$ grep sys_nanosleep . -R
./include/libc_private.h:int __sys_nanosleep(const struct timespec *, struct timespec *);
./sys/Symbol.map: __sys_nanosleep;
./sys/interposing_table.c: SLOT(nanosleep, __sys_nanosleep),
./sys/nanosleep.c:__weak_reference(__sys_nanosleep, __nanosleep);
lib/libc/sys/interposing_table.c 파일에 따르면 nanosleep 함수는 __sys_nanosleep 함수와 연결되어 있다. FreeBSD 규칙(convention)에 따라 __sys로 시작하는 함수는 시스템 호출 함수이다.
lib/libc/include/libc_private.h 파일은 시스템 호출 주변의 래퍼 함수에 필요한 비공개 및 내부 함수 선언을 포함한다.

커널 링은 시스템 호출을 통해 커널의 기능을 제공하는 것이 주요 목적이다. 다음은 리눅스 같은 모놀리식 커널(Monolthic kernel)의 커널 프로세스와 사용자 프로세스를 비교한 것이다.
exec, fork 시스템 호출을 이용해 생성된다.운영체제의 런타임에는 커널 프로세스 모드(커널 랜드, 커널 공간), 사용자 프로세스 모드(유저 랜드, 사용자 공간) 두 개의 실행 모드가 존재한다.
다음은 유닉스 커널이 갖는 권한의 목록이다.

연결된 모든 하드웨어는 유닉스 시스템에 연결된 장치로 간주된다. 장치는 필수(mandatory) 장치, 그리고 주변(peripheral) 장치로 구분된다. CPU와 메인 메모리는 필수 장치, 하드디스크 드라이브나 네트워크 어댑터, 마우스, 모니터 등 나머지 모든 하드웨어는 주변 장치이다.
유닉스 커널은 필수 장치를 완벽하게 은닉하며 사용자 공간에서는 접근이 허용되지 않는다. 하지만 주변 장치는 장치 파일이라는 메커니즘을 통해 노출된다. 이는 /dev 경로에서 확인할 수 있다.
셀 박스 10-6 리눅스 머신의 /dev 내용 목록
ls -l /dev
total 0
crw-r--r-- 1 root root 10, 235 Sep 6 20:27 autofs
drwxr-xr-x 2 root root 700 Sep 6 20:58 block
crw------- 1 root root 10, 234 Sep 6 20:27 btrfs-control
drwxr-xr-x 3 root root 60 Sep 6 20:27 bus
drwxr-xr-x 2 root root 4600 Sep 6 20:27 char
crw------- 1 root tty 5, 1 Sep 6 20:27 console
lrwxrwxrwx 1 root root 11 Sep 6 20:27 core -> /proc/kcore
drwxr-xr-x 14 root root 280 Sep 6 20:27 cpu
crw------- 1 root root 10, 260 Sep 6 20:27 cpu_dma_latency
crw------- 1 root root 10, 261 Sep 6 20:27 cpu_wakeup_latency
crw------- 1 root root 10, 203 Sep 6 20:27 cuse
drwxr-xr-x 11 root root 220 Sep 6 20:27 disk
drwxr-xr-x 2 root root 60 Sep 6 20:27 dma_heap
drwxr-xr-x 3 root root 100 Sep 6 20:27 dri
...
유닉스에서 하드웨어 장치에 대한 추상화는 가상 장치를 가질 수 있게 하기 때문에, 위 결과는 모두 물리 장치가 아니다. 예를 들어 가상 네트워크 어댑터 장치는 네트워크 패킷에 추가적인 작업을 수행함으로써 VPN 메커니즘의 일부가 될 수 있다.