10.5 RIO 패키지를 사용한 Robust I/O
🔹 개요
Robust는 '견고한'이라는 의미로, 여기서는 입력 데이터의 이상치나 노이즈에 덜 민감하다는 의미로 사용되었다.
RIO 패키지는 자동적으로 shourt counts 다루는 패키지로, short counts를 해야 하는 네트워크 프로그램 등의 응용프로그램에서 편리하고, robust하고, 효율적인 입출력을 제공한다.
Short counts는 예상보다 짧은 입력 데이터가 들어온 상황을 의미한다. 다음과 같은 상황이 short counts에 해당된다.
- read 도중 EOF(end-of-file)을 만난 경우
- text lines를 터미널로부터 read할 때
- 네트워크 소켓 통신 시
RIO는 2가지 다른 종류의 함수를 제공한다.
- 이진 데이터의 Unbuffered input and output : rio_readn, rio_writen
- 텍스트 라인과 이진 데이터의 Buffered input : rio_readlineb, rio_readnb
🔹 목적
Unix의 기본 read/write 시스템 호출은 일시적인 인터럽션이나 short read/short write 문제에 취약하다. 이를 보완하기 위해 Rio가 필요하다.
🔹 핵심 함수
- ssize_t rio_readn(int fd, void *usrbuf, size_t n)
- 정확히 n 바이트를 읽어 들이려고 시도함
- read()의 short count 문제를 반복 호출로 처리
- 반환값은 실제 읽은 바이트 수 (0 이상), 오류 시 -1
- ssize_t rio_writen(int fd, void *usrbuf, size_t n)
- 정확히 n 바이트를 쓰려고 시도
- short write에 대해 내부적으로 반복 호출
- 반환값은 쓴 바이트 수 또는 -1
- ssize_t rio_readlineb(rio_t *rp, void *usrbuf, size_t maxlen)
- 파일 디스크립터로부터 텍스트 한 줄을 읽어옴
- 내부적으로 버퍼링된 읽기를 사용
- 줄 끝에 도달하거나 maxlen보다 크면 종료
- ssize_t rio_readnb(rio_t *rp, void *usrbuf, size_t n)
- 버퍼를 사용하여 n 바이트까지 robust하게 읽어옴
- 내부적으로 rio_t 구조체의 버퍼에서 데이터를 채움
🔹 rio_t 구조체
- 내부 버퍼와 파일 디스크립터를 포함
- 버퍼링된 읽기를 위한 상태 정보 관리
- rio_readinitb(rio_t *rp, int fd)를 통해 초기화
🔹 장점
- 시스템 호출의 인터럽트 대응
- short count 문제 해결
- read/write보다 사용이 안정적이고 예측 가능
🔹 Rio 예제
#include "csapp.h"
int main(int argc, char **argv) {
int n;
rio_t rio;
char buf[MAXLINE];
Rio_readinitb(&rio, STDIN_FILENO);
while((n = Rio_readlineb(&rio, buf, MAXLINE)) != 0)
Rio_writen(STDOUT_FILENO, buf, n);
exit(0);
}
11.5 웹 서버
11.5.1 웹 기초
🔹 웹 클라이언트와 서버는 HTTP(Hypertext Transfer Protocol) 기반의 텍스트 기반 애플리케이션 계층 프로토콜로 통신한다.
🔹 HTTP의 동작 구조: 브라우저(클라이언트)가 서버에 연결 요청을 보냄 서버가 콘텐츠를 반환함
🔹 연결 종료 후, 클라이언트가 받은 데이터를 화면에 출력
🔹 HTML은 웹 콘텐츠를 구성하는 언어
🔹 웹은 FTP 등 기존 파일 전송 서비스와 달리, 문서 자체에 사용자와의 상호작용을 위한 구조를 포함할 수 있음

- HTTPHypertext Transfer Protocol
- 웹 클라이언트와 서버가 상호 작용하는 텍스트 기반 응용 프로그램 수준 프로토콜
- 간단한 프로토콜
- 웹 클라이언트(브라우저)가 서버에 연결을 열고 콘텐츠를 요청한다.
- 서버는 요청된 콘텐츠를 응답하고 연결을 닫는다.
- 브라우저는 콘텐츠를 읽고 화면에 표시한다.
- HTTP의 특징
- HTTP 메세지는 HTTP 서버와 HTTP 클라이언트에 의해 해석된다.
- TCP/IP를 이용하는 응용 프로토콜이다.
- HTTP는 연결 상태를 유지하지 않는 비연결성 프로토콜이다.
- HTTP는 연결을 유지하지 않는 프로토콜이기 때문에 요청/응답 방식으로 동작한다.
- 웹 서비스와 FTP와 같은 기존 파일 검색 서비스의 차이점
-
-
- 주요 차이점은 웹 콘텐츠가 HTML이라는 언어로 작성될 수 있다는 점이다.
-

- HTMLHypertext Markup Language
- HTML 프로그램(페이지)은 페이지의 다양한 텍스트 및 그래픽 객체를 어떻게 표시해야 하는지에 대한 지침(태그)을 포함한다.
- 태그 예시
- <B> Make me bold! </B>: 텍스트를 굵게 표시하도록 브라우저에게 지시한다.
- 하이퍼링크 기능
- HTML의 진정한 강점은 인터넷 호스트에 저장된 콘텐츠에 대한 포인터(하이퍼링크)를 포함할 수 있다는 점이다.
- <a href="http://www.cmu.edu/index.html">Carnegie Mellon</a>: 'Carnegie Mellon' 텍스트 객체를 강조 표시하고, CMU 웹 서버에 저장된 'index.html'이라는 HTML 파일에 대한 하이퍼링크를 생성한다.
- 사용자가 강조 표시된 텍스트 객체를 클릭하면 브라우저는 CMU 서버로부터 해당 HTML 파일을 요청하고 표시한다.
11.5.2 웹 컨텐츠
🔹 콘텐츠는 바이트의 시퀀스로 표현되며 MIME 타입이 함께 지정됨
└ 예: text/html, image/jpeg, text/plain 등
🔹 웹 콘텐츠 종류
├ 정적 콘텐츠: 디스크 파일을 읽어 클라이언트에 전송 (예: .html, .jpg 등)
└ 동적 콘텐츠: 실행 파일을 실행하여 그 출력을 클라이언트로 전송 (예: CGI 스크립트 등)
🔹 각각의 콘텐츠는 URL(Uniform Resource Locator) 로 식별됨 구조
├ 예: http://www.google.com:80/index.html
└ 포트번호는 기본값 80일 경우 생략 가능
🔹 동적 URL은 ?로 구분된 인자 포함 가능 (예: http://host/cgi-bin/adder?15000&213)
웹 클라이언트와 서버에게 콘텐츠content는 연결된 MIMEmultipurpose internet mail extensions 타입을 가진 바이트의 시퀀스이다. MIME 타입은 문서, 파일, 바이트 스트림의 성격과 포맷을 나타내는 표준화된 방법으로, 클라이언트에게 전송되는 데이터의 유형을 알려줘서 적절한 방식으로 처리할 수 있게 한다. 아래 표는 몇 가지 일반적인 MIME 타입을 보여준다.
| MIME type | Description |
| text/html | HTML page |
| text/plain | Unformatted text |
| application/postscript | Postscript document |
| image/gif | Binary image encoded in GIF format |
| image/png | Binary image encoded in PNG format |
| image/jpeg | Binary image encoded in JPEG format |
웹 서버는 두 가지 다른 방식으로 클라이언트에게 콘텐츠를 제공한다.
- 정적 콘텐츠 제공
- 디스크 파일을 가져와 그 내용을 클라이언트에게 반환하는 방식
- 이 디스크 파일을 정적 콘텐츠static content라고 부름
- 파일을 클라이언트에게 반환하는 과정을 정적 콘텐츠 제공serving static content이라고 부름
- 동적 콘텐츠 제공
- 실행 파일을 실행하고 그 출력을 클라이언트에게 반환하는 방식
- 실행 파일이 실행 시 생성하는 출력을 동적 콘텐츠dynamic content라고 부름
- 프로그램을 실행하고 그 출력을 클라이언트에게 반환하는 과정을 동적 콘텐츠 제공serving dynamic content이라고 부름
웹 컨텐츠는 웹 서버에 의해 관리되는 파일과 연관되어 있다. 이러한 각 파일은 URLuniversal resource locator이라는 고유한 이름을 가진다.
- URL의 구조:
- 예를 들어, URL http://www.google.com:80/index.html는 포트 80에서 리스닝하는 웹 서버가 관리하는 인터넷 호스트 http://www.google.com에 있는 /index.html이라는 HTML 파일을 식별한다.
- 포트 번호는 선택 사항이며 기본값은 잘 알려진 HTTP 포트 80이다.
- 실행 파일의 URL은 파일 이름 뒤에 프로그램 인자를 포함할 수 있다. '?' 문자는 파일 이름과 인자를 구분하며, 각 인자는 '&' 문자로 구분된다.
- 클라이언트와 서버의 URL 사용
- 클라이언트는 URL의 접두사(예: http://www.google.com:80)를 사용하여 어떤 종류의 서버에 연결할지(HTTP), 서버가 어디에 있는지(www.google.com), 어떤 포트에서 리스닝하는지(80) 결정한다.
- 서버는 URL의 접미사(예: /index.html)를 사용하여 파일 시스템에서 파일을 찾고 요청이 정적 콘텐츠인지 동적 콘텐츠인지 판단한다.
서버가 URL의 접미사를 어떻게 해석하는지에 대해 이해할 몇 가지 사항이 있다.
- 정적/동적 판단 규칙
- URL이 정적 콘텐츠를 참조하는지 동적 콘텐츠를 참조하는지 결정하는 표준 규칙은 없다. 각 서버는 자신이 관리하는 파일에 대한 고유한 규칙을 가지고 있다. 고전적인 접근 방식은 모든 실행 파일이 위치해야 하는 cgi-bin과 같은 디렉토리 집합을 식별하는 것이다.
- 접미사의 시작 '/'
- 접미사의 시작 '/'는 리눅스 루트 디렉토리를 의미하지 않는다. 오히려 요청되는 콘텐츠 종류의 홈 디렉토리를 의미한다. 예를 들어, 서버는 모든 정적 콘텐츠가 디렉토리 /usr/httpd/html에 저장되고 모든 동적 콘텐츠가 디렉토리 /usr/httpd/cgi-bin에 저장되도록 구성될 수 있다.
- 최소 URL 접미사 '/'
- 최소 URL 접미사는 '/' 문자이며, 모든 서버는 이를 /index.html과 같은 기본 홈페이지로 확장한다. 이것이 브라우저에 도메인 이름만 입력하여 사이트의 홈페이지를 가져올 수 있는 이유를 설명한다. 브라우저는 누락된 '/'를 URL에 추가하여 서버에 전달하고, 서버는 '/'를 기본 파일 이름으로 확장한다.
11.5.3 HTTP 트랜잭션
🔹 HTTP 요청과 응답은 텍스트 라인 기반 포맷을 사용함
🔹 요청 예시 (telnet을 통해 시뮬레이션 가능) :
GET / HTTP/1.1
Host: www.aol.com
🔹 응답 예시 :
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 42092
<html> ... </html>
🔹 HTTP 요청 구조 :
method URI version (예 : GET /index.html HTTP/1.1)
🔹 HTTP 응답 구조:
version status-code status-message
🔹 상태 코드
├ 200 OK : 정상 처리
├ 404 Not Found : 파일 없음
└ 501 Not Implemented : 메서드 미지원
🔹 telnet을 사용해 디버깅할 수 있으며, 헤더 줄은 \r\n으로 끝나야 한다.
- telnet의 용도
- 텍스트 기반 프로토콜을 사용하는 서버(예: 웹 서버)와 통신하고 디버깅하는 데 유용하다.
- 원격 로그인 도구로서보다는 서버 디버깅에 더 적합하다.
linux> telnet www.aol.com 80 // Client: open connection to server
Trying 205.188.146.23... // Telnet prints 3 lines to the terminal
Connected to aol.com.
Escape character is ’^]’.
GET / HTTP/1.1 // Client: request line
Host: www.aol.com // Client: required HTTP/1.1 header
// Client: empty line terminates headers
HTTP/1.0 200 OK // Server: response line
MIME-Version: 1.0 // Server: followed by five response headers
Date: Mon, 8 Jan 2010 4:59:42 GMT
Server: Apache-Coyote/1.1
Content-Type: text/html // Server: expect HTML in the response body
Content-Length: 42092 // Server: expect 42,092 bytes in the response body
// Server: empty line terminates response headers
<html> // Server: first HTML line in response body
... // Server: 766 lines of HTML not shown
</html> // Server: last HTML line in response body
Connection closed by foreign host. // Server: closes connection
linux> // Client: closes connection and terminates
- telnet을 사용한 웹 서버 트랜잭션 과정
- 실행: 리눅스 셸에서 telnet을 실행하고 웹 서버(aol.com)에 포트(예: 80)로 연결 요청을 한다 (라인 1)
- 초기 출력: telnet은 터미널에 세 줄의 출력을 인쇄하고 연결을 연 다음 텍스트 입력을 기다린다 (라인 5)
- 텍스트 입력 및 전송: 사용자가 텍스트 라인을 입력하고 Enter 키를 누르면, telnet은 해당 라인을 읽고 캐리지 리턴과 라인 피드 문자(\r\n)를 추가하여 서버에 전송한다.
- HTTP 표준: HTTP 표준은 모든 텍스트 라인이 캐리지 리턴 및 라인 피드 쌍으로 끝나야 한다고 요구한다.
- 트랜잭션 시작: HTTP 요청을 입력하여 트랜잭션을 시작한다 (라인 5–7)
- 서버 응답: 서버는 HTTP 응답으로 회신한다 (라인 8–17)
- 연결 종료: 응답 후 서버는 연결을 닫는다 (라인 18)
🔹 HTTP Requests
- 클라이언트가 서버에게 정보를 달라고 연락 보내는 것을 요청이라고 하며, 메세지를 보낼 때 요청에 대한 정보를 담아 서버로 보낸다.
- HTTP 요청은 0개 이상의 request headers가 따라 오는 request line다.
- Request line : <method> <uri> <version>
- <method> : GET, POST, OPTIONS, HEAD, PUT, DELTE, TRACE 중의 하나
- GET은 서버에게 URIUniform Resource Identifier에 의해 식별되는 내용을 리턴할 것을 지시한다.
- POST는 자료의 생성을, PUT은 자료의 수정을, DELETE는 자료의 삭제를 요청할 때 사용한다.
- HEAD는 GET의 요청과 동일한 응답을 요구하지만, 응답 본문을 포함하지 않는다. (헤더 정보만 리턴함) (참고)
- <uri> : 일반적으로 프록시를 위한 URL, 서버를 위한 URL suffix
- <version> : 요청의 HTTP 버전 (HTTP/1.0 또는 HTTP/1.1)
- <method> : GET, POST, OPTIONS, HEAD, PUT, DELTE, TRACE 중의 하나
- Request head : <header name>: <header data>
- 서버에 추가적인 정보를 제공함
HTTP POST 요청에서 인자의 전달
HTTP POST 요청을 위한 인자들은 URI보다는 request body에 전달된다.
🔹 HTTP Responses
- HTTP 응답은 0개 이상의 response header와 컨텐츠, 그리고 헤더와 컨텐츠를 분리하는 빈 라인 \r\n이 따라오는 response line이다.
- Response line : <version> <status code> <status msg>
- <version> : 응답의 HTTP 버전
- <status code> : 숫자로 된 상태 (3비트 양수)
- <status msg> : 상태 코드에 대응하는 영어 텍스트
| Status code | Status message | Description |
| 200 | OK | Request was handled without error. |
| 301 | Moved permanently | Content has moved to the hostname in the Location header. |
| 400 | Bad request | Request could not be understood by the server. |
| 403 | Forbidden | Server lacks permission to access the requested file. |
| 404 | Not found | Server could not find the requested file. |
| 501 | Not implemented | Server does not support the request method. |
| 505 | HTTP version not supported | Server does not support version in request. |
- Response headers : <header name>: <header data>
- response에 대한 추가적인 정보 제공
- Content-Type: response body의 MIME 타입
- Content-Length: response body의 content의 길이
11.5.4 동적 컨텐츠의 처리
🔹 정적 콘텐츠 제공
- 파일을 열고 HTTP 응답 헤더 + 바디를 전송
- MIME 타입은 파일 확장자로 결정
🔹 동적 콘텐츠 제공 (CGI 기반)
- CGICommon Gateway Interface : 웹 서버와 외부 프로그램 간 상호작용 규약
- CGI는 웹 서버와 외부 프로그램 간의 표준 방법을 제공하며 주로 웹 서버에서 동적인 콘텐츠를 생성하기 위해 사용됨
- 동작 방식
- 웹 서버는 특정 요청이 CGI 스크립트에 의해 처리되어야 함을 인식함
- 서버는 CGI 스크립트를 실행하고 HTTP 요청 정보를 스크립트에 전달함
- CGI 스크립트는 요청을 처리하고 결과를 웹 서버에 반환함
- 웹 서버는 이 결과를 클라이언트에 전송함
- GET 요청의 인자들은 URL에서 ? 뒤에 포함됨 → 서버는 이를 환경변수 QUERY_STRING에 저장
- CGI 프로그램은 서버의 자식 프로세스로 실행됨
- fork → dup2(fd, STDOUT_FILENO) → execve(program)
- CGI 프로그램의 출력은 클라이언트로 전송됨
CGI 프로그램이 실행되는 프로세스가 로드되기 전에, 리눅스의 dup2 함수를 사용하여 표준 출력을 클라이언트와 연결된 디스크립터로 리디렉션한다. 따라서 CGI 프로그램이 표준 출력에 쓰는 모든 내용은 클라이언트에게 직접 전달된다.
- CGI 환경 변수의 예
| Environment variable | Description |
| QUERY_STRING | Program arguments |
| SERVER_PORT | Port that the parent is listening on |
| REQUEST_METHOD | GET or POST |
| REMOTE_HOST | Domain name of client |
| REMOTE_ADDR | Dotted-decimal IP address of client |
| CONTENT_TYPE | POST only: MIME type of the request body |
| CONTENT_LENGTH | POST only: Size in bytes of the request body |
- (예) 두 개의 정수를 더하는 CGI 프로그램
#include "csapp.h"
int main(void) {
char *buf, *p;
char arg1[MAXLINE], arg2[MAXLINE], content[MAXLINE];
int n1=0, n2=0;
/* Extract the two arguments */
if ((buf = getenv("QUERY_STRING")) != NULL) {
p = strchr(buf, ’&’);
*p = ’\0’;
strcpy(arg1, buf);
strcpy(arg2, p+1);
n1 = atoi(arg1);
n2 = atoi(arg2);
}
/* Make the response body */
sprintf(content, "QUERY_STRING=%s", buf);
sprintf(content, "Welcome to add.com: ");
sprintf(content, "%sTHE Internet addition portal.\r\n<p>", content);
sprintf(content, "%sThe answer is: %d + %d = %d\r\n<p>", content, n1, n2, n1 + n2);
sprintf(content, "%sThanks for visiting!\r\n", content);
/* Generate the HTTP response */
printf("Connection: close\r\n");
printf("Content-length: %d\r\n", (int)strlen(content));
printf("Content-type: text/html\r\n\r\n");
printf("%s", content);
fflush(stdout);
exit(0);
}
- 위 프로그램으로부터 동적 HTML 컨텐츠를 제공하는 HTTP 트랜잭션
linux> telnet kittyhawk.cmcl.cs.cmu.edu 8000 // open connection
Trying 128.2.194.242... // Telnet prints 3 lines to the terminal
Connected to aol.com.
Escape character is ’^]’.
GET /cgi-bin/adder?15000&213 HTTP/1.0 // request line
// empty line terminates headers
HTTP/1.0 200 OK // response line
Server: Tiny Web Server // identify server
Content-length: 115 // expect 115 bytes in response body
Content-type: text/html // expect HTML in response body
// empty line terminates headers
Welcome to add.com: THE Internet addition portal. // first HTML line
<p>The answer is: 15000 + 213 = 15213 // second HTML line in response body
<p>Thanks for visiting! // third HTML line in response body
Connection closed by foreign host. // closes connection
linux> // closes connection and terminates
[How Web Servers Work]
🔹 서버는 요청 수신 후 다음 중 하나 수행
├ 정적 콘텐츠이면: 디스크에서 파일 읽기 → 응답 전송
└ 동적 콘텐츠이면: CGI 프로그램 실행 → 그 출력을 전송
🔹 URL 해석 방식은 서버에 따라 다름
├ 일반적으로 cgi-bin 디렉터리에 있는 파일을 CGI로 처리함
└ URI의 /는 리눅스 루트가 아니라 서버 콘텐츠 디렉터리 기준임
🔹 예시
/index.html → ./home.html
/cgi-bin/adder?15000&213 → ./cgi-bin/adder 실행
💡 브라우저는 기본 URL만 입력해도 /index.html을 유추해서 요청함
11.6 TINY 웹 서버
🔹 구조
├ 1. 소켓 초기화 (open_listenfd)
├ 2. accept()로 클라이언트 연결 수락
├ 3. HTTP 요청 파싱
└ 4. serve_static() 또는 serve_dynamic() 호출
🔹 동적 콘텐츠 처리
──────────────────
if (Fork() == 0) {
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO);
Execve(filename, emptylist, environ);
}
Wait(NULL);
──────────────────
├ execve로 CGI 프로그램 실행
├ stdout을 소켓으로 리다이렉션
└ 부모는 wait()로 자식 종료 기다림
🔹 개요
Tiny는 약 250줄의 C 코드로 구현된 간단한 반복형(Iterative) 웹 서버다.
- HTTP/1.0 프로토콜 지원
- 정적/동적 콘텐츠 제공
- GET 요청만 처리
- CGI 기반 동적 실행 지원
🔹 main 함수 (서버 실행 흐름)
/*
* tiny.c - A simple, iterative HTTP/1.0 Web server that uses the
* GET method to serve static and dynamic content
*/
#include "csapp.h"
void doit(int fd);
void read_requesthdrs(rio_t *rp);
int parse_uri(char *uri, char *filename, char *cgiargs);
void serve_static(int fd, char *filename, int filesize);
void get_filetype(char *filename, char *filetype);
void serve_dynamic(int fd, char *filename, char *cgiargs);
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg);
int main(int argc, char **argv) {
int listenfd, connfd;
char hostname[MAXLINE], port[MAXLINE];
socklen_t clientlen;
struct sockaddr_storage clientaddr;
/* Check command-line args */
if (argc != 2) {
fprintf(stderr, "usage: %s <port>\n", argv[0]);
exit(1);
}
listenfd = Open_listenfd(argv[1]);
while (1) {
clientlen = sizeof(clientaddr);
connfd = Accept(listenfd, (SA *)&clientaddr, &clientlen);
Getnameinfo((SA *) &clientaddr, clientlen, hostname, MAXLINE,
port, MAXLINE, 0);
printf("Accepted connection from (%s, %s)\n", hostname, port);
doit(connfd);
Close(connfd);
}
}
- 사용자가 명령줄 인자로 포트 번호를 입력해야 함
- open_listenfd(argv[1]): 수신 소켓 생성
- 메인 루틴 흐름
- open_listenfd 함수를 호출하여 리스닝 소켓을 연다.
- 무한 서버 루프를 실행한다.
- 무한 루프 동작
- 연결 요청을 반복적으로 수락한다 (라인 32).
- 트랜잭션을 수행한다 (라인 36).
- 연결의 서버 측을 닫는다 (라인 37).
🔹 doit 함수 – 요청 처리 핵심
void doit(int fd) {
int is_static;
struct stat sbuf;
char buf[MAXLINE], method[MAXLINE], uri[MAXLINE], version[MAXLINE];
char filename[MAXLINE], cgiargs[MAXLINE];
rio_t rio;
/* Read request line and headers */
Rio_readinitb(&rio, fd);
Rio_readlineb(&rio, buf, MAXLINE);
printf("Request headers:\n");
printf("%s", buf);
sscanf(buf, "%s %s %s", method, uri, version);
if (strcasecmp(method, "GET")) {
clienterror(fd, method, "501", "Not implemented",
"Tiny does not implement this method");
return;
}
read_requesthdrs(&rio);
/* Parse URI from GET request */
is_static = parse_uri(uri, filename, cgiargs);
if (stat(filename, &sbuf) < 0) {
clienterror(fd, filename, "404", "Not found",
"Tiny couldn’t find this file");
return;
}
if (is_static) { /* Serve static content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IRUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn’t read the file");
return;
}
serve_static(fd, filename, sbuf.st_size);
}
else { /* Serve dynamic content */
if (!(S_ISREG(sbuf.st_mode)) || !(S_IXUSR & sbuf.st_mode)) {
clienterror(fd, filename, "403", "Forbidden",
"Tiny couldn’t run the CGI program");
return;
}
serve_dynamic(fd, filename, cgiargs);
}
}
- 실행 흐름:
- 요청 라인 읽기 (GET /url HTTP/1.0)
- read_requesthdrs로 헤더 무시
- parse_uri로 정적/동적 여부 및 파일/인자 분리
- 파일 존재 여부 확인 (stat)
- 정적이면 serve_static, 동적이면 serve_dynamic 호출
- doit 함수 역할: 하나의 HTTP 트랜잭션을 처리한다.
- 요청 라인 처리: rio_readlineb 함수를 사용하여 요청 라인을 읽고 구문 분석한다 (라인 11-14).
- 메소드 확인: GET 메소드만 지원하며, 다른 메소드 요청 시 오류 메시지를 보내고 메인 루틴으로 복귀한다 (라인 15-19).
- 헤더 처리: 요청 헤더를 읽고 무시한다 (라인 20).
- URI 구문 분석: URI를 파일 이름과 CGI 인자 문자열로 구문 분석하고, 정적/동적 콘텐츠 요청 여부를 나타내는 플래그를 설정한다 (라인 23).
- 파일 존재 확인: 요청된 파일이 디스크에 존재하는지 확인하고, 없으면 오류 메시지를 보내고 반환한다.
- 정적 콘텐츠 처리:
- 요청이 정적 콘텐츠인 경우, 파일이 일반 파일이고 읽기 권한이 있는지 확인한다 (라인 31).
- 조건 충족 시 정적 콘텐츠를 클라이언트에게 제공한다 (라인 36).
- 동적 콘텐츠 처리:
- 요청이 동적 콘텐츠인 경우, 파일이 실행 가능한지 확인한다 (라인 39).
- 조건 충족 시 동적 콘텐츠를 제공한다 (라인 44).
🔹 clienterror – 오류 응답 처리
void clienterror(int fd, char *cause, char *errnum,
char *shortmsg, char *longmsg) {
char buf[MAXLINE], body[MAXBUF];
/* Build the HTTP response body */
sprintf(body, "<html><title>Tiny Error</title>");
sprintf(body, "%s<body bgcolor=""ffffff"">\r\n", body);
sprintf(body, "%s%s: %s\r\n", body, errnum, shortmsg);
sprintf(body, "%s<p>%s: %s\r\n", body, longmsg, cause);
sprintf(body, "%s<hr><em>The Tiny Web server</em>\r\n", body);
/* Print the HTTP response */
sprintf(buf, "HTTP/1.0 %s %s\r\n", errnum, shortmsg);
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-type: text/html\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Content-length: %d\r\n\r\n", (int)strlen(body));
Rio_writen(fd, buf, strlen(buf));
Rio_writen(fd, body, strlen(body));
}
- HTML 형식의 오류 메시지 생성 후 전송
- 403, 404, 501 등의 상황에 호출됨
- robust I/O로 출력 전송 (에러 발생도 클라이언트에 친절히 알림)
- clienterror 함수 역할: 클라이언트에게 오류를 보고한다.
- 오류 처리 수준: 실제 서버에 비해 제한적이다.
- 응답 내용
- HTTP 응답을 클라이언트에게 보낸다.
- 응답 라인에 적절한 상태 코드와 상태 메시지를 포함한다.
- 응답 본문에 오류 설명을 위한 HTML 파일을 포함한다.
- HTML 콘텐츠 구성
- HTML 콘텐츠를 단일 문자열로 만들어 크기 결정이 용이하게 한다.
- HTML 응답은 본문의 크기와 타입을 표시해야 하기 때문이다.
- 출력 함수: 모든 출력에 rio_writen 함수를 사용한다.
🔹 read_requesthdrs
void read_requesthdrs(rio_t *rp) {
char buf[MAXLINE];
Rio_readlineb(rp, buf, MAXLINE);
while(strcmp(buf, "\r\n")) {
Rio_readlineb(rp, buf, MAXLINE);
printf("%s", buf);
}
return;
}
- HTTP 요청 헤더는 실제로 사용하지 않음
- 단순히 \r\n 빈 줄까지 읽고 출력
- robust I/O 활용: Rio_readlineb() 사용
- read_requesthdrs 함수 역할: 요청 헤더를 읽고 무시한다.
- 사용 목적: Tiny 서버는 요청 헤더의 정보를 사용하지 않으므로, 단순히 읽고 버린다.
- 헤더 종료 방식: 요청 헤더는 캐리지 리턴 및 라인 피드(\r\n) 쌍으로 구성된 빈 라인으로 끝난다.
- 종료 확인: 함수는 이 빈 라인을 확인하여 헤더의 끝을 판단한다.
🔹 parse_uri
int parse_uri(char *uri, char *filename, char *cgiargs) {
char *ptr;
if (!strstr(uri, "cgi-bin")) { /* Static content */
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
if (uri[strlen(uri)-1] == ’/’)
strcat(filename, "home.html");
return 1;
}
else { /* Dynamic content */
ptr = index(uri, ’?’);
if (ptr) {
strcpy(cgiargs, ptr+1);
*ptr = ’\0’;
} else
strcpy(cgiargs, "");
strcpy(filename, ".");
strcat(filename, uri);
return 0;
}
}
- cgi-bin 포함 여부로 정적/동적 구분
- 정적:
- filename = "." + uri
- /로 끝나면 "home.html" 추가
- 동적:
- ? 기준 인자 분리 → cgiargs
- filename = "." + uri(앞부분)
- 정적 콘텐츠 요청 처리 (라인 5):
- CGI 인자 문자열을 지운다 (라인 6).
- URI를 ./index.html과 같은 상대적인 리눅스 경로 이름으로 변환한다 (라인 7-8).
- URI가 '/' 문자로 끝나면 (라인 9), 기본 파일 이름(./home.html)을 추가한다 (라인 10).
- 동적 콘텐츠 요청 처리 (라인 13):
- CGI 인자를 추출하고 (라인 14-20), URI의 나머지 부분을 상대적인 리눅스 파일 이름으로 변환한다 (라인 21-22).
- cgi-bin 문자열이 포함된 URI는 동적 콘텐츠 요청으로 간주된다.
🔹 serve_static
void serve_static(int fd, char *filename, int filesize) {
int srcfd;
char *srcp, filetype[MAXLINE], buf[MAXBUF];
/* Send response headers to client */
get_filetype(filename, filetype);
sprintf(buf, "HTTP/1.0 200 OK\r\n");
sprintf(buf, "%sServer: Tiny Web Server\r\n", buf);
sprintf(buf, "%sConnection: close\r\n", buf);
sprintf(buf, "%sContent-length: %d\r\n", buf, filesize);
sprintf(buf, "%sContent-type: %s\r\n\r\n", buf, filetype);
Rio_writen(fd, buf, strlen(buf));
printf("Response headers:\n");
printf("%s", buf);
/* Send response body to client */
srcfd = Open(filename, O_RDONLY, 0);
srcp = Mmap(0, filesize, PROT_READ, MAP_PRIVATE, srcfd, 0);
Close(srcfd);
Rio_writen(fd, srcp, filesize);
Munmap(srcp, filesize);
}
/*
* get_filetype - Derive file type from filename
*/
void get_filetype(char *filename, char *filetype) {
if (strstr(filename, ".html"))
strcpy(filetype, "text/html");
else if (strstr(filename, ".gif"))
strcpy(filetype, "image/gif");
else if (strstr(filename, ".png"))
strcpy(filetype, "image/png");
else if (strstr(filename, ".jpg"))
strcpy(filetype, "image/jpeg");
else
strcpy(filetype, "text/plain");
}
- MIME 타입 결정: .html, .jpg, .png, .gif 등
- HTTP 응답 헤더 생성 후 전송
- mmap으로 파일을 메모리에 매핑 후 전송
- 전송 후 munmap으로 해제
- serve_static 함수 역할
- 파일 유형 결정: 파일 이름의 접미사를 검사하여 파일 유형을 결정한다 (라인 7).
- 응답 헤더 전송: 응답 라인과 응답 헤더를 클라이언트에게 보낸다 (라인 8-13). 헤더는 빈 줄로 끝난다.
- 응답 본문 전송: 요청된 파일 내용을 연결된 디스크립터 fd로 복사하여 응답 본문을 보낸다. 이 과정은 다음과 같이 수행된다.
- 읽기 위해 filename을 연다 (라인 18).
- 리눅스 mmap 함수가 요청된 파일을 가상 메모리 영역에 매핑한다 (라인 19). mmap 호출은 파일 srcfd의 첫 filesize 바이트를 srcp 주소에서 시작하는 사설 읽기 전용 가상 메모리 영역으로 매핑한다.
- 파일을 메모리에 매핑한 후, 더 이상 파일 디스크립터가 필요 없으므로 파일을 닫는다 (라인 20). 이를 잊으면 잠재적으로 치명적인 메모리 누수를 유발할 수 있다.
- 실제 파일 전송은 rio_writen 함수가 수행한다 (라인 21). rio_writen은 srcp 위치(요청된 파일에 매핑된 곳)에서 시작하는 filesize 바이트를 클라이언트의 연결된 디스크립터로 복사한다.
- 마지막으로 매핑된 가상 메모리 영역을 해제한다 (munmap, 라인 22). 이 또한 잠재적으로 치명적인 메모리 누수를 방지하기 위해 중요하다.
- 출력 함수: 모든 출력에 강력한 rio_writen 함수를 사용한다.
🔹 serve_dynamic
void serve_dynamic(int fd, char *filename, char *cgiargs) {
char buf[MAXLINE], *emptylist[] = { NULL };
/* Return first part of HTTP response */
sprintf(buf, "HTTP/1.0 200 OK\r\n");
Rio_writen(fd, buf, strlen(buf));
sprintf(buf, "Server: Tiny Web Server\r\n");
Rio_writen(fd, buf, strlen(buf));
if (Fork() == 0) { /* Child */
/* Real server would set all CGI vars here */
setenv("QUERY_STRING", cgiargs, 1);
Dup2(fd, STDOUT_FILENO); /* Redirect stdout to client */
Execve(filename, emptylist, environ); /* Run CGI program */
}
Wait(NULL); /* Parent waits for and reaps child */
}
- CGI 방식: 자식 프로세스에서 프로그램 실행
- QUERY_STRING 환경 변수 설정
- 표준 출력을 connfd로 리다이렉트 (dup2)
- execve()로 CGI 프로그램 실행
- serve_dynamic 함수 역할: 동적 콘텐츠를 제공한다.
- 작동 방식: 자식 프로세스를 포크하여 CGI 프로그램을 실행한다.
- 초기 응답: 성공 응답 라인과 Server 헤더를 클라이언트에게 보낸다. (나머지 응답은 CGI 프로그램의 책임이다.)
- 오류 처리: CGI 프로그램 오류에 대한 처리가 부족하여 견고성이 떨어진다.
- 자식 프로세스 생성 및 설정:
- 새로운 자식 프로세스를 포크한다 (라인 11).
- 자식은 요청 URI의 인자로 QUERY_STRING 환경 변수를 초기화한다 (라인 13). (실제 서버는 다른 CGI 변수도 설정한다.)
- 표준 출력 리디렉션: 자식의 표준 출력을 클라이언트와 연결된 디스크립터로 리디렉션한다 (라인 14).
- CGI 프로그램 실행: 자식은 execve를 사용하여 CGI 프로그램을 로드하고 실행한다 (라인 15).
- 데이터 흐름: CGI 프로그램의 표준 출력은 부모의 개입 없이 클라이언트에게 직접 전달된다.
- 부모 프로세스 동작: 부모는 자식이 종료될 때까지 wait 호출에서 블록되어 기다린다 (라인 17).
🔹 예제: adder.c (CGI 프로그램)
// adder?15000&213 요청 → HTML로 15213 반환
char *buf = getenv("QUERY_STRING"); // "15000&213"
int n1 = atoi(...), n2 = atoi(...); // 파싱 및 계산
printf("Content-type: text/html\r\n\r\n...") // 결과 전송

11.7 웹 프록시
웹 프록시Web proxy는 웹 브라우저와 엔드 서버(오리지널 서버) 사이에서 중간자 역할을 하는 프로그램이다. 웹 페이지를 얻기 위해 엔드 서버에 직접 접속하는 대신, 브라우저는 프록시에 접속하고, 프록시는 이 요청을 엔드 서버로 전달한다. 엔드 서버가 프록시에 응답하면, 프록시는 그 응답을 브라우저로 다시 보낸다.

- 클라이언트에게는 프록시가 서버처럼 동작한다.
- 서버에게는 프록시가 클라이언트처럼 동작한다.
프록시는 다음과 같은 기능이나 역할을 수행할 수 있다.
- 방화벽 기능: 방화벽 뒤의 브라우저가 프록시를 통해 외부 서버와 통신하도록 한다.
- 익명화: 요청에서 식별 정보를 제거하여 브라우저를 익명으로 만든다.
- 캐싱: 서버의 객체를 로컬에 저장(캐시)하고, 이후 동일한 요청에 대해 캐시된 복사본으로 응답하여 원격 통신을 줄인다.
프록시는 request와 response를 지나치면서 캐싱, 로깅, 익명화, 필터링, 트랜스코딩 등의 유용한 기능을 수행할 수 있다.

👨🔬 랩 프로젝트 단계
- Implementing a sequential web proxy
- 프록시가 들어오는 연결을 수락하도록 설정한다.
- 요청을 읽고 구문 분석한다.
- 웹 서버로 요청을 전달한다.
- 서버의 응답을 읽어 해당 클라이언트에게 전달한다.
- 기본 HTTP 작동 및 소켓 사용법 학습이 포함된다.
- Dealing with multiple concurrent requests
- 여러 개의 동시 연결을 처리하도록 프록시를 업그레이드한다.
- 동시성 개념을 다룬다.
- Caching web objects
- 메인 메모리 캐시를 사용해서 최근 접근한 웹 콘텐츠를 캐싱하는 간단한 캐싱 기능을 추가한다.
11.7.1 Sequential Web Proxy 구현하기
프록시가 시작되면 명령줄에서 지정된 포트 번호로 들어오는 연결 요청을 기다려야 한다. 연결이 설정되면 프록시는 클라이언트로부터 요청 전체를 읽고 요청을 구문 분석해야 한다. 클라이언트가 유효한 HTTP 요청을 보냈는지 판단해야 하며, 유효한 요청이라면 적절한 웹 서버에 자체적으로 연결을 설정한 후 클라이언트가 지정한 객체를 요청해야 한다. 마지막으로 프록시는 서버의 응답을 읽어 해당 클라이언트에게 전달해야 한다.
1️⃣ HTTP/1.0 GET request
최종 사용자가 웹 브라우저 주소창에 http://www.cmu.edu/hub/index.html와 같은 URL을 입력하면, 브라우저는 프록시에게 다음과 같은 줄로 시작하는 HTTP 요청을 보낼 것이다.
GET http://www.cmu.edu/hub/index.html HTTP/1.1
이 경우, 프록시는 요청을 최소한 다음 필드로 구문 분석해야 한다: 호스트 이름 http://www.cmu.edu, 그리고 경로 또는 쿼리 및 그 뒤의 것(/hub/index.html) 이렇게 하면 프록시는 http://www.cmu.edu에 연결을 열고 다음과 같은 형태의 줄로 시작하는 자체 HTTP 요청을 보내야 함을 결정할 수 있다.
GET /hub/index.html HTTP/1.0
HTTP 요청의 모든 줄은 캐리지 리턴(\r) 뒤에 개행 문자(\n)가 온다는 점에 유의해야 한다. 또한 중요한 것은 모든 HTTP 요청이 빈 줄(\r\n)로 끝난다는 것이다.
위 예시에서 웹 브라우저의 요청 라인은 HTTP/1.1로 끝나지만, 프록시의 요청 라인은 HTTP/1.0으로 끝난다는 점을 알아야 한다. 최신 웹 브라우저는 HTTP/1.1 요청을 생성하지만, 프록시는 이를 처리하고 HTTP/1.0 요청으로 전달해야 한다.
2️⃣ Request headers
브라우저가 HTTP 요청의 일부로 어떤 추가적인 요청 헤더를 전송하는 경우, 프록시는 이를 변경 없이 그대로 전달해야 한다.
- 항상 Host 헤더를 전송해야 한다.
- 이 동작은 기술적으로 HTTP/1.0 사양에 의해 승인되지 않지만, 특정 웹 서버, 특히 가상 호스팅을 사용하는 서버로부터 합리적인 응답을 얻기 위해 필요하다.
- Host 헤더는 최종 서버의 호스트 이름을 설명한다. 예를 들어, http://www.cmu.edu/hub/index.html에 접근하기 위해 프록시는 다음 헤더를 전송할 것이다:
- Host: http://www.cmu.edu
- 웹 브라우저가 자체적인 Host 헤더를 HTTP 요청에 포함할 수 있다. 만약 그렇다면, 프록시는 브라우저와 동일한 Host 헤더를 사용해야 한다.
- 항상 다음 User-Agent 헤더를 전송하도록 선택할 수 있다:
- User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3)
Gecko/20120305 Firefox/10.0.3 - 이 헤더는 작성 편의상 두 개의 별도 줄로 제공되었지만, 프록시는 이 헤더를 단일 줄로 전송해야 한다.
- User-Agent 헤더는 클라이언트(운영 체제 및 브라우저와 같은 매개변수 측면)를 식별하며, 웹 서버는 종종 이 식별 정보를 사용하여 제공하는 콘텐츠를 조작한다. 이 특정 User-Agent 문자열을 전송하는 것은 간단한 telnet 스타일 테스트 중에 얻는 자료의 콘텐츠와 다양성을 향상시킬 수 있다.
- 편의 상 User-Agent 헤더의 값은 proxy.c에 문자열 상수로 제공된다.
- User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:10.0.3)
- 항상 다음 Connection 헤더를 전송해야 한다:
- Connection: close
- 항상 다음 Proxy-Connection 헤더를 전송해야 한다:
- Proxy-Connection: close
- Connection 및 Proxy-Connection 헤더는 첫 번째 요청/응답 교환이 완료된 후 연결이 유지될지 여부를 지정하는 데 사용된다. 요청마다 새로운 연결을 여는 것은 완벽하게 허용 가능하다. 이 헤더들의 값으로 close를 지정하는 것은 프록시가 첫 번째 요청/응답 교환 후에 연결을 닫을 의도임을 웹 서버에게 알린다.
3️⃣ Port 숫자
- HTTP 요청 포트
- HTTP 요청의 URL에서 선택적인 필드이다.
- 즉, URL은 http://www.cmu.edu:8080/hub/index.html 형태일 수 있으며, 이 경우 프록시는 기본 HTTP 포트인 80 포트 대신 8080 포트의 호스트 http://www.cmu.edu에 연결해야 한다. 포트 번호가 URL에 포함되어 있든 그렇지 않든 프록시는 제대로 작동해야 한다.
- 리스닝 포트
- 프록시가 들어오는 연결 요청을 기다리는 포트이다. 프록시는 명령줄 인자로 프록시의 리스닝 포트 번호를 지정받아야 한다. 예를 들어, 다음 명령으로 프록시는 15213 포트에서 연결을 기다려야 한다
- linux> ./proxy 15213
- 다른 프로세스에서 사용되지 않는 한 어떤 비특권 리스닝 포트(1,024보다 크고 65,536보다 작은)라도 선택할 수 있다. 각 프록시는 고유한 리스닝 포트를 사용해야 하고 많은 사람이 각 머신에서 동시에 작업할 것이므로, 사용자의 개인 포트 번호를 선택하는 데 도움을 주기 위해 port-for-user.pl 스크립트가 제공된다. 사용자 ID 기반으로 포트 번호를 생성하는 데 사용해라:
- linux> ./port-for-user.pl droh
droh: 45806
port-for-user.pl에 의해 반환되는 포트 p는 항상 짝수이다. 따라서 추가적인 포트 번호가 필요하다면, 예를 들어 Tiny 서버를 위해, 포트 p와 p + 1을 안전하게 사용할 수 있다.
11.7.2 다중 concurrent requests 처리하기
- 작업하는 순차 프록시가 있으면, 여러 요청을 동시에 처리하도록 수정해야 한다. 동시 서버를 구현하는 가장 간단한 방법은 새로운 연결 요청마다 새 스레드를 생성하는 것이다. 12.5.5에 설명된 미리 스레드된 서버와 같은 다른 설계도 가능하다.
- 스레드는 메모리 누수를 피하기 위해 detached 모드로 실행해야 한다는 점에 유의해라.
- open_clientfd 및 open_listenfd 함수는 현대적이고 프로토콜 독립적인 getaddrinfo 함수를 기반으로 하므로 스레드 안전하다.
11.7.3 웹 오브젝트 캐싱하기
HTTP는 실제로 웹 서버가 자신이 제공하는 객체를 어떻게 캐시해야 하는지에 대한 지침을 제공하고 클라이언트는 캐시를 자신을 위해 어떻게 사용해야 하는지를 지정할 수 있는 상당히 복잡한 모델을 정의한다. 그러나 프록시는 단순화된 접근 방식을 채택할 것이다.
프록시가 서버로부터 웹 객체를 수신할 때, 객체를 클라이언트에게 전송하면서 메모리에 캐시해야 한다. 다른 클라이언트가 동일한 서버로부터 동일한 객체를 요청하는 경우, 프록시는 서버에 다시 연결할 필요가 없다. 단순히 캐시된 객체를 재전송할 수 있다.
프록시가 요청되는 모든 객체를 캐시한다면 무제한의 메모리가 필요할 것이다. 특히 어떤 웹 객체는 다른 객체보다 더 크기 때문에, 하나의 거대한 객체가 전체 캐시를 소비하여 다른 객체들이 전혀 캐시되지 못하게 할 수도 있다. 이러한 문제를 피하기 위해 프록시는 최대 캐시 크기와 최대 캐시 객체 크기를 모두 가져야 한다.
1️⃣ 최대 캐시 크기
- 프록시 캐시 전체는 MAX_CACHE_SIZE = 1 MB 의 최대 크기를 가져야 한다.
- 캐시 크기를 계산할 때, 프록시는 실제 웹 객체를 저장하는 데 사용된 바이트만 계산해야 한다. 메타데이터를 포함한 모든 부가적인 바이트는 무시해야 한다.
2️⃣ 최대 객체 크기
- 프록시는 MAX_OBJECT_SIZE = 100 KB를 초과하지 않는 웹 객체만 캐시해야 한다.
- 올바른 캐시를 구현하는 가장 쉬운 방법은 각 활성 연결에 대해 버퍼를 할당하고 서버로부터 데이터를 수신하면서 누적하는 것이다.
- 만약 버퍼 크기가 최대 객체 크기를 초과하면, 버퍼는 폐기될 수 있다.
- 만약 웹 서버 응답 전체가 최대 객체 크기를 초과하기 전에 읽힌다면, 해당 객체는 캐시될 수 있다.
- 이 방식을 사용하면, 프록시가 웹 객체를 위해 사용할 수 있는 최대 데이터 양은 다음과 같으며, 여기서 T는 최대 활성 연결 수이다: MAX_CACHE_SIZE + T * MAX_OBJECT_SIZE
3️⃣ 축출 정책
- 프록시 캐시는 최소 LRUleast-recently-used 교체 정책에 근사하는 교체 정책을 사용해야 한다.
4️⃣ 동기화
- 캐시에 대한 접근은 스레드 안전해야 하며, 캐시 접근이 경쟁 상태로부터 자유롭도록 보장해야 한다. 실제로, 여러 스레드가 동시에 캐시로부터 읽을 수 있어야 한다는 특별한 요구 사항이 있다. 물론 한 번에 하나의 스레드만 캐시에 쓸 수 있도록 허용되어야 하지만, 읽는 스레드에 대해서는 그런 제한이 존재해서는 안 된다.
- 따라서 하나의 큰 배타적 잠금으로 캐시 접근을 보호하는 것은 허용 가능한 해결책이 아니다. 캐시 분할, Pthreads 읽기-쓰기 잠금 사용 또는 세마포어를 사용하여 자체적인 읽기-쓰기 해결책을 구현하는 옵션을 탐색해 볼 수 있다. 어떤 경우든, 엄격한 LRU 교체 정책을 구현할 필요가 없다는 사실이 여러 읽는 스레드를 지원하는 데 있어 약간의 유연성을 제공할 것이다.
'Krafton Jungle > 4. CSAPP' 카테고리의 다른 글
| [Computer System] ⑫ 동시성 프로그래밍 (2) (0) | 2025.05.05 |
|---|---|
| [Computer System] ⑫ 동시성 프로그래밍 (1) (0) | 2025.05.05 |
| [Computer System] ⑪ 네트워크 프로그래밍 (1) (0) | 2025.05.01 |
| [Computer System] ⑨ 가상메모리 (2) (0) | 2025.04.24 |
| [Computer System] ⑨ 가상메모리 (1) (0) | 2025.04.22 |