티스토리 뷰
멀티플렉싱
소켓 통신을 구현하는 방법 중 멀티플렉싱이 있다.
- 멀티플렉싱이란 다중요청을 모아서 한번에 처리한다는 것이다.
- 지금까지 배운 방법에서는 (아직 멀티프로세스나, 멀티스레드의 개념을 배우지 않았기 때문에) 클라이언트가 접속을 요청하고 처리 할때마다 최소 서버와 클라이언트가 1:1로 필요했다. 즉 클라이언트 10개가 접속하면 서버도 10개가 존재해야 다중요청을 처리 할 수 있었다.
- 추가로 하나의 클라이언트는 최소한 2개의 동작, 전송과 수신을 동시에 처리해야하는 경우까지 더하면 더 많은 자원이 필요하다.
- 이것을 해결하는 방법은 여러가지가(멀티프로세스, 스레드 등) 있지만 그 중에서 select라는 linux systemcall 함수를 통해서 멀티플렉싱 방법을 배워보자.(윈도우에도 동일한 select가 존재한다. 잘 안쓰이고 linux와의 호환성을 위해서 존재하는 것 같기는 하지만...)
fd_set 구조체
- select함수를 사용하기 위해서는 fd를 fd_set구조체에 등록해야한다.
- fd_set구조체 자체 기능이나 제공하는 매크로도 있지만, 비유하자면 select 함수는 은행이고, fd_set 구조체는 대기표를 뽑는 것과 같다고 생각하면 된다.(물론 사람이 없다면 대기표를 안뽑겠지만 개념상으로 이해하자)
- fd_set fdset; 이렇게 선언해서 사용하고 기본적으로 필요한 함수들은
- FD_ZERO(fd_set* fs) : 전달된 fs의 메모리 모두 0으로 초기화
- FD_SET(int fd, fd_set* fs) : fd 번호를 fs에 등록(fs의 있는 array[fd]의 bit를 1로 바꾼다.)
- FD_CLR(int fd, fd_set* fs) : fd 번호를 fs에서 삭제(fs의 있는 array[fd]의 bit를 0로 바꾼다.)
- FD_ISSET(int fd, fd_set* fs) : fd 번호가 fs에 존재하는지 확인(fs의 있는 array[fd]의 bit가 1인지 확인.)
fd_set 제공 매크로 추가 설명
- FD_ZERO(fd_set* fs)는 memset하고 동일하다. gcc 내부코드를 뜯어보니
#define FD_ZERO(p) __DARWIN_FD_ZERO(p)
#define __DARWIN_FD_ZERO(p) __builtin_bzero(p, sizeof(*(p)))
- __builtin_bzero 이걸 사용하던데 검색해보니 memset과 동일하지만, 비표준 함수이기 때문에 사용하지 말라고 한다.
- fdset 구조체는 1024개의 fd를 등록할 수 있다. sizeof를 해보면 128bit가 나온다.
- 1bit로 1개의 fd를 관리할 수 있다. 근데 이걸 알게 된 후, 그럼 어떤 fd가 어디에 있는지는 어떻게 확인하는 걸까?라는 의문이 들었다.
- 왜냐면 fd의 번호는 1024를 넘어서도 생성이 될 수 있기 때문이다.(하단에 설명을 따로 했지만. 결론은 1024개 이상 fd는 확인 못한다.
- fd_set구조체의 최대 관리 fd는 1021개이고(표준 fd 때문), 최대 fd번호도 1023을 넘는 fd번호는 관리 할 수 없다.
- FD_ISSET, SET, CLR, ZERO 매크로 함수들은 bit연산으로 관리하는 것 같다. 하나씩 살펴보면 FD_ZERO는 memset과 동일한 기능일테고, FD_SET은 fd번호에 해당하는 bit 값을 1로 바꾼다. 즉 fd_set구조체에 1024개의 배열중에서 fd번호에 해당하는 bit가 1로 바꾼다. 연산으로 적자면 bit << (fd + 1) = 1 이거일듯(+1은 fd는 0부터 시작이라서)
- FD_CLR은 FD_SET했던 bit를 0으로 만든다. 근데 사실 FD_SET안한 파일디스크립트 전달해도 아무 의미 없어보인다.
- 연산이 해당 bit를 0으로 만드는 것이고 fd_set 구조체는 아무 fd를 등록하지 않아도 기본적으로 128bit를 차지하기 때문이다. 연산은 아마도 bit << (fd + 1) = 0 이거 하는게 끝일 거 같다.
- FD_ISSET은 전달된 fd번호와 & 연산을 하는 것 같다. 1024 bit니까 한번의 연산마다 1024개씩 반복문을 돌겠지만, bit연산은 아주 간단한 연산이니까 크게 비용이 높은 연산은 아닌 것 같다.
- bit << (fd + 1) & fd_set[i]을 해서 나오는 결과 bit가 1이면 해당 값이 존재하는 것이다.(select함수 호출 뒤의 FD_ISSET호출은 해당 fd에 변화를 확인해서 그 다음 처리를 할 수 있게 해준다.(클라이언트 접속이나, 메세지 송신, 수신)
- 설명 진짜 길고 조금 헷갈렸는데, 이해를 하니까 되게 간단하다.
select를 사용순서
- fd 생성
- fd_set 구조체에 fd등록
- 이때 목적에 따라서 fd_set을 만들어야한다. read, write, error 최대 3가지 분류로 등록이 가능하므로
- 생성되는 fd_set도 3개 만들어 질 수 있다.
- timeout을 설정할 timeval 구조체 설정(정말 간단하니까 검색ㄱㄱ)
- 생성한 fd_set을 목적에따라서 read, write, error인자로 할당(3개 다 해도 되고 1개만 해도 된다)
- select 함수 호출
- 결과 확인
select 함수 호출 후 fd_set
- select 함수를 호출하면 read, write, error 3개의 인자로 전달된 fd_set은 모두 0으로 초기화 된다.
- 이때 변화(소켓이라면 send, receive, error 파일이라면 read, write, error)가 생긴 fd_set의 비트만 1로 바뀐다.
- 그래서 위에서 설명한 FD_ISSET(int fd, fd_set* fs)를 하면 어떤 fd(소켓)이 변화했는지 알 수 있다.
- 변화한 fd를 발견하면 그 후의 처리를 하면 된다.(파일 읽기나, 소켓 전송 등)
내가 생각하는 select의 장점.
- 처음에는 무슨개념인지 잘 몰랐는데. 책이나 인터넷에서 설명을 들은대로, io를 모아서 관리하겠다. 이게 전부였다. 그리고 개념적으로는 꽤 좋은 것 같다.
- 이 io를 모아서 관리해서 좋은 점은, 멀티프로세스나 멀티스레드는 클라이언트가 메세지를 전송, 수신 하는 것을 하나의 process(thread)로는 불가능 했는데(blocking 소켓이라고 가정했을 때) io를 모아서 관리하므로 i나 o가 따로 발생해도 처리가 가능하고, io가 같이 발생해도 처리가 가능해졌다.
- 구현이 좀 간단해보인다. 서버나 클라이언트를 만들고 read, write, error fd를 생성해서 select로 변화 감지를 하면 blocking 되지 않는 구조로 쉽게 만들 수 있을 것 같다.(물론 아무리봐도 nonblocking은 아닌 것 같다. block과 nonblock의 사이정도?)
내가 생각하는 select의 단점.
- 책에서도 써 있었던 것 같았는데 select 함수 호출 이후에 무거운 연산을 하는 프로그램에서는 적용하기 힘들어보인다. 이유는 위에서도 썼듯이 100% nonblock이 아니기 떄문이다.
- 책에서 본 코드를 보면 이런 순서로 처리가 진행된다.
- fd생성과 timeout 등은 됐다고 가정하고 현재 프로그램이 작동되고 있다고 가정해보자.
- select 함수를 호출한다.
- 변경감지, 시간초과, 에러 3가지 중 하나의 결과가 미리 정한 timeout내에 발생한다.
- 보통 시간초과면 그냥 다시 select함수를 호출하는 구조로 작성한다. 에러는 따로 처리한다고 가정하자.
- 변경이 발생하면 해당 변경에 대한 fd처리 연산을 한다.
- 3번의 연산을 하는 동안 fd에 또다른 전송, 수신, 에러 등 변화가 발생할 것이다.
- 그렇지만 당장은 어떤 변화가 발생해도 처리할 수 없다. 처리는 select 함수를 호출하고나서 처리가 가능하다.(여기서 또 의문이 들었는데 select를 호출해서 클라이언트의 메세지를 받는다면, read나 recv 함수를 사용할텐데 이 함수를 사용하는 도중에 계속해서 클라이언트가 메세지를 전송한다면 어떻게 되는걸까? 이런 애매한 부분이 분명 있다.)(근데 조금만 더 생각해보니 저번에 알게 된 클라이언트의 send와 서버의 read의 속도차가 발생해서 생기는 문제점을 해결하는 방법과 동일하게 해결하면 되긴 할거 같다. header를 만들어서 전송하면 되겠다.)
- 그래서 select함수 호출 후에 생기는 변화는 다음 select함수 호출 시 확인이 가능하다.
- 4번이 제일 큰 문제이자 단점 같다. 만약 select함수 호출 후 어떤처리가 10초이상 걸리는 연산을 한다고 가정해보자. 그러면 우리 서버는 10초동안 다른 어떤 처리도 불가능하다.
- 직접 테스트 해봤는데 select후 sleep(10); 을 하면 10초동안 다른 클라이언트의 접속, 메세지 전송 등등 아무것도 안된다.
- 위와 같은 이유로 상대적으로 조금은 작은 프로젝트나 트래픽이 발생하는 서버에서 사용하라고 설명하는 것 같다.
fd_set의 최대fd 개수와 심각한 단점.
- fd_set 구조체를 몇개를 만들어도 사용할 수 있는 파일디스크립터는 최대 1024개 fd번호는 1023까지고 012는 표준 입출력 fd이므로 프로그래머가 최대로 사용 가능한 개수는 1021개라고 보면 된다. 아직 안배워서 잘 모르겠지만 그래서 epoll이라는 것이 나왔다고 하는 것 같다.
- fd_set 구조체를 select에서 사용하고 등록하는데 딱 보니 비트연산을 이용하는데 만약 fd번호가 1024가 넘어가면 어떻게 등록하는 거지? 라는 의문점이 있었는데... 1023번 이상의 fd(소켓)이 생기면 해당 프로세스에서는 사용불가다.(FD_ISSET(1024, &reads)를 해도 응답이 없다.) 추가로 사용하려면 결국 새로운 fd를 생성할 수 있는 프로세스를 생성해야한다.(스레드는 안된다. 스레드는 결국 같은 메모리를 공유하므로 fd나 소켓을 생성하면 스레드 10개가 10번씩 생성한다고 하면 마지막 fd번호는 102 가 될것이다.) 프로세스는 pipe 전에는 생성 fd번호를 공유하지만 pipe 후에는 fd 번호를 공유하지 않는다.
내가 생각한 select함수 내부 작동 원리.
- select는 linux os가 제공해주는 system call이다. 즉 최종적인 관리는 결국 os가 한다.(윈도우에도 구조가 조금 다르지만 동일한 기능이 있다.)
- fd생성 fd_set구조체 설정, timeout 설정을 완료하고 select함수를 호출하면 이런 방식으로 내부 구현이 되어있지 않을까 해서 적어본다.
- 변경을 감지할 count 생성과 timeout 시간 측정 시작
- FD_ZERO(struct fd_set*) 실행
- 첫번째 인자로 전달된 max만큼의 배열값까지 fd_set내부의 배열을 반복한다.
- 이때 변경된 fd가 있다면 해당 배열값을 1로 바꾸고 count++
- max값 까지 반복문을 모두 돌았다면 count와 timeout 체크
- count > 0 이면 return count;, timeout이면 return 0
- 그리고 이건 시간을 측정해야하는 함수이기 때문에 아마도 시간은 따로 thread가 체크해서 해당 시간이 지나면 반복문을 돌고 있더라도 중지시키고 0으로 리턴하는 부분이 존재하지 않을까라고 생각해본다.
- 말로 설명하니까 번잡하니까 코드로 설명해보자.
int select(int max, struct fd_set* reads, struct fd_set* writes, struct fd_set* errors, struct timeval* timeout){
long time = currtime;
int count = 0;
FD_ZERO(&reads);
FD_ZERO(&writes);
FD_ZERO(&errors);
while(true){
for(int i = 0; i < max; i++){
if(checkRead(i) == read){
reads[i] = 1;
count++;
continue;
}
if(checkWrite(i) == write){
writes[i] = 1;
count++;
continue;
}
if(checkError(i) == error){
errors[i] = 1;
count++;
continue;
}
}
if(currtime > time + timeout){
return 0;
}
if(count > 0){
return count;
}
}
}
- 설명만을 위한 거의 슈도코드 수준으로 짜봤다.
- 대충 이런 구조로 되어있을 것 같다. 물론 훨씬 예외처리나 시간검사가 복잡하게 되어있겠지만...
- checkRead, Write, Error 함수에서 전달된 i와 비트연산을 하거나 나는 반복문을 돌리는 것으로 적었지만 max까지의 비트만큼 비트연산으로 내부구조가 되어 있을 수도 있다고 생각한다.
- 내부 구조를 살짝 봤는데 내가 생각한 것과 거의 비슷하게 되어있었다. 다만 하나 더 고려해야할 것으로는 반복문을 도는 동안 timeout이 발생했을 때의 처리가 되어있을텐데 그건 어떻게 처리되는건지 코드만 봐서는 잘 모르겠다. 아마도 시간 측정은 다른 thread나 systemcall에서 하고 지나면 signal을 보내주는 형식이지 않을까??
- 난 read, write, error를 3개 따로 검사했는데 구현 코드를 보니까 3개를 따로 받지만 실제로는 하나의 구조체로 fd.in, fd.out, fd.er 뭐 이런식으로 받아서 처리한다.
그림 설명
- 책에는 이렇게 그려져 있다.
- 내가 select 함수를 이해하고 그린 구조의 그림은 이렇다.
- 이름이 멀티플렉싱이라서 뭔가 복잡할거 같지만, select함수를 배운 내 느낌은 배열 1024개 만들고(물론 bit배열로 최대한 작게) 크기만큼 반복으로 읽어서 변화를 확인하고 알려준다. 이게 전부다.
결론
- 처음에는 뭔가 되게 어려울 것 같았는데 계속 보고 뜯어보니까 그렇게 어렵지 않은 방법이라고 느껴졌다. 그러면서도 io를 같이(동시 라고 적고 싶었는데 동시처리는 아니다.)처리할 수 있는 방법을 제공하는 것이 맘에들고 구현도 쉬울 것 같아서 조금 더 맘에 들었다.
- 스레드로 다중채팅도 구현해봤지만 스레드는 아직은 어렵다. 아니 언제까지 어려울지 모르겠다. ㅠㅠ
'C\\C++' 카테고리의 다른 글
[C/C++] restrict 키워드 (0) | 2023.01.24 |
---|---|
[C/C++] 표준입출력 함수의 장점과 단점 (0) | 2023.01.21 |
[C/C++] 10, 2, 16, 8 진수 변환 규칙 (0) | 2023.01.07 |
[C/C++] 가변인자 함수 (1) | 2023.01.04 |
[C/C++] 조건부 컴파일(#if #ifdef #ifndef #endif···) (0) | 2023.01.01 |
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
- Total
- Today
- Yesterday
링크
TAG
- Til
- ajax 403에러
- C언어
- MySql 날짜 차이 구하기
- JPQL 사용하기
- 네트워크 오더링
- c 가변인자
- Double.compareTo(Double)
- 나만의 강점
- 이중 콜론 연산자
- ##연산자
- c++ 가변인자
- Builder #SuperBuilder
- 매크로
- c 매크로
- 전처리기
- static의 장점 단점
- #define
- 표준입출력 함수
- JPA #SPRING #ENTITY #DATABASE
- javascript 문자열 뒤집기
- JPA #cascade
- linux select
- Java8 #java stream
- C++
- Java Double형 비교방법
- java
- 메소드 참조 연산자
- JavaScript
- 영속성전파
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | |
7 | 8 | 9 | 10 | 11 | 12 | 13 |
14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 | 22 | 23 | 24 | 25 | 26 | 27 |
28 | 29 | 30 |
글 보관함