여기서 server는 accept와 recv 할 때 효율적 대기를 하는것인가? 그러면여기서 mutex와 signal은 누가 주지?
->아주 날카롭고 본질적인 질문입니다!
결론부터 말씀드리면, 효율적인 대기(Sleep/Blocked)가 맞습니다. 하지만 애플리케이션 코드처럼 우리가 직접 pthread_mutex_t나 pthread_cond_t를 사용하는 것이 아니라, OS 커널(Kernel) 내부의 대기 큐(Wait Queue)와 하드웨어 인터럽트(Interrupt) 시스템에 의해 동작합니다.
네, 맞습니다.
accept()나 recv()를 호출해서 블로킹 상태에 빠졌을 때, CPU는 무한 루프(while(1))를 돌며 바쁘게 대기(Busy Waiting / Spin Lock)하는 것이 아닙니다.
OS 커널이 해당 스레드의 상태를 TASK_INTERRUPTIBLE (Blocked/Sleep 상태)로 전환하고 CPU 스케줄링 대상에서 완전히 제외합니다. 따라서 CPU 사용률은 0%가 됩니다.
우리가 사용자 공간(User Space) 코드에서 C언어로 pthread_cond_wait()을 부를 때와 비유해 보면 구조가 완벽히 대응됩니다.
| 개념 | 애플리케이션 (User Space) | 소켓 통신 (Kernel & Hardware) |
|---|---|---|
| 대기 장소 | Condition Variable (pthread_cond_t) | 커널 내부 소켓의 Wait Queue (대기 큐) |
| Wait (재우기) | pthread_cond_wait() | accept(), recv() 시스템 콜 |
| Signal (깨우기) | pthread_cond_signal() | 랜카드(NIC)의 하드웨어 인터럽트 & 커널 네트워크 스택 |
| 동기화 (Lock) | pthread_mutex_t | 커널 내부의 Spinlock / Socket Lock |
클라이언트가 데이터를 보내서 서버의 recv()가 깨어나는 과정은 아래와 같이 진행됩니다.
recv() 호출 (Sleep 전환)recv() 시스템 콜을 호출합니다.wake_up() 함수(유저 공간의 Signal과 동일한 역할)를 불러 스레드를 Ready (Runnable) 상태로 깨웁니다.recv() 함수 실행을 완료하고, 데이터를 버퍼로 받아와 다음 C 코드를 계속 실행합니다.Wait Queue)에 깨우기 신호(wake_up)를 보냅니다.#client.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h> // 소켓 생성 및 통신 관련 함수 (socket, connect, send, recv 등)
#include <arpa/inet.h> // IP 주소 변환 및 바이트 순서 변환 함수 (htons, inet_pton 등)
#define PORT 8080
#define BUF_SIZE 1024
int main(void) {
// 1. TCP 소켓 생성 (IPv4, TCP 통신)
int sock_fd = socket(AF_INET, SOCK_STREAM, 0);
if (sock_fd == -1) {
perror("socket");
exit(1);
}
// 2. 서버 주소 정보 구조체 설정 (IPv4, Port 8080)
struct sockaddr_in server_addr = { 0 }; // 쓰레기 값 방지를 위한 초기화
server_addr.sin_family = AF_INET; // IPv4 주소 체계
server_addr.sin_port = htons(PORT); // Port 번호를 네트워크 바이트 순서(Big-Endian)로 변환
// 문자열 IP 주소("127.0.0.1")를 바이너리 네트워크 주소로 변환하여 server_addr.sin_addr에 저장
if (inet_pton(
AF_INET,
"127.0.0.1",
&server_addr.sin_addr
) != 1){
perror("inet_pton");
close(sock_fd);
exit(1);
}
// 3. 서버에 TCP 연결 요청 (3-way handshake 수행)
if (connect(
sock_fd,
(struct sockaddr *)&server_addr,
sizeof(server_addr)
) == -1) {
perror("connect");
close(sock_fd);
exit(1);
}
printf("connected to server\n");
const char *message = "hello server";
// 4. 서버로 메시지 전송
if (send(
sock_fd,
message,
strlen(message),
0
) == -1) {
perror("send");
close(sock_fd);
exit(1);
}
char buffer[BUF_SIZE] = {0};
// 5. 서버로부터 응답 수신 (데이터가 올 때까지 대기/Blocking)
ssize_t n = recv(
sock_fd,
buffer,
sizeof(buffer) - 1, // 문자열 끝 null 문자를 남겨두기 위해 -1
0
);
if (n == -1) {
perror("recv");
close(sock_fd);
exit(1);
}
// 수신한 데이터 끝에 null 문자('\0')를 붙여 안전한 문자열로 완결
buffer[n] = '\0';
printf("server response: %s\n", buffer);
// 6. 소켓 자원 해제 및 연결 종료
close(sock_fd);
return 0;
}
#server.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#define PORT 8080
#define BUF_SIZE 1024
int main(void) {
int server_fd = socket(AF_INET, SOCK_STREAM, 0);
if (server_fd == -1) {
perror("socket");
exit(1);
}
//열려있다면, 무시하고 다시 덮어씀
int opt = 1;
if (setsockopt(
server_fd,
SOL_SOCKET,
SO_REUSEADDR,
&opt,
sizeof(opt)
) == -1) {
perror("setsockopt");
close(server_fd);
exit(1);
}
//IPv4 주소 구조체 생성, 설정
struct sockaddr_in addr = { 0 };
addr.sin_family = AF_INET;
addr.sin_port = htons(PORT);
addr.sin_addr.s_addr = INADDR_ANY;
//설정한 주소 정보를 서버 소켓에 바인딩
if (bind(server_fd, (struct sockaddr *)&addr, sizeof(addr)) == -1) {
perror("bind");
close(server_fd);
exit(1);
}
//소켓을 연결 수신 대기 상태로 전환
if (listen(server_fd, 5) == -1) {
perror("listen");
close(server_fd);
exit(1);
}
printf("listening of port %d...\n", PORT);
// 여기서 block
int client_fd = accept(server_fd, NULL,NULL);
if (client_fd == -1) {
perror("accept");
close(server_fd);
exit(1);
}
printf("client connected\n");
printf("server_fd = %d\n", server_fd);
printf("client_fd = %d\n", client_fd);
char buffer[BUF_SIZE] = { 0 };
ssize_t n = recv(
client_fd,
buffer,
sizeof(buffer) - 1,
0
);
if (n == -1) {
perror("recv");
close(client_fd);
close(server_fd);
exit(1);
}
buffer[n] = '\0';
printf("recieved: %s\n", buffer);
const char *response = "hello client";
if (send(
client_fd,
response,
strlen(response),
0
) == -1) {
perror("send");
}
close(client_fd);
close(server_fd);
return 0;
}
이번 server.c / client.c (B5 TCP Socket 기초) 실습에서 질문하셨던 핵심 개념과 답변들을 한눈에 복습 및 기록하실 수 있도록 요약·정리해 두었습니다.
sockaddr_in 구조체 멤버명 sin_family의 의미sin = socket internet (인터넷 소켓)family = 주소 체계 / 계열struct sockaddr_in 구조체 내부 멤버의 접두어(sin_)이며, sin_family = AF_INET은 IPv4 인터넷 주소 체계를 사용하겠다는 의미입니다.&server_addr.sin_addr의 역할server_addr 구조체 내부의 IP 주소 필드(sin_addr)에 대한 메모리 주소(Pointer)입니다.inet_pton(AF_INET, "127.0.0.1", &server_addr.sin_addr)와 같이, 문자열 IP를 네트워크 바이너리로 변환한 결과를 어느 메모리 위치에 저장할지 넘겨주는 목적으로 사용됩니다.accept 등)가 아니라 OS 커널의 TCP/IP 스택이 백그라운드에서 실행합니다.listen()을 호출해 LISTEN 상태가 된 후, 클라이언트가 connect()를 호출할 때 일어납니다.accept()의 실제 역할: Handshake를 직접 하는 것이 아니라, 커널 내부 Accept Queue에 이미 Handshake가 완료되어 들어온 연결을 꺼내와서 애플리케이션용 File Descriptor(client_fd)를 반환합니다.recv() / accept()에서의 스레드 블로킹 (Blocking)Waiting (Blocked) 상태로 전환됩니다.send, recv)는 그 결과를 애플리케이션으로 가져오는 인터페이스 역할을 합니다.send() 2번 예시의 의미: 작성하신 클라이언트 코드는 send()를 1번만 부르고 있으나, "만약 클라이언트가 send()를 2번 호출한다면?"이라는 가상의 예시입니다.send()를 2번 나누어 부르더라도 네트워크 상황이나 OS 버퍼 상태에 따라 서버는 recv() 1번으로 통째로 받거나 여러 번 쪼개서 받을 수 있습니다. 따라서 추후 Protocol Framing(구분자 추가, 길이 명시 등) 처리 로직이 필요합니다.recv() 호출 시 sizeof(buffer) - 1을 사용하는 이유'\0')을 추가할 1바이트 공간을 확보하기 위함입니다.recv()는 데이터 뒤에 NULL 문자를 자동으로 붙여주지 않습니다. 만약 버퍼 크기 전체(1024)를 수신받으면 buffer[n] = '\0' 시 메모리 침범(Buffer Overflow)이 발생하므로, 최대 1023바이트만 받도록 제한하여 안전하게 문자열로 완결 짓습니다.recv() 함수의 추상화 수준recv()는 하드웨어(NIC) 인터럽트, IP/TCP 헤더 검증, Checksum, 패킷 재정렬, 손실 재전송, 커널 ➔ 유저 메모리 복사 등 복잡한 네트워크 스택의 처리를 C 함수 호출 한 줄로 은닉한 고급 추상화 API입니다.