client 가 연결을 요청을 하면, accept 함수를 이용하여,
client 와 연결되는 새로운 socket 을 동작시킨다.
이 accept 라는 함수는 기본적으로 blocking 함수이다.
만약 listening socket 에 연결된 연결 요청 대기 큐에
아무런 요청이 없다면,
이 accept 함수는 client 의 연결 요청이 있는 시점까지, 그 다음의 code 를 진행시키지 않고 대기하게 된다.
그런데 이 blocking 으로 동작하는 accept 함수가 새로운 client 의 요청을 기다리고 있는 동안에 이 프로세스가 signal(신호) 를 받아서, 기존 signal함수 또는 sigaction 함수로 등록해둔, signal handler 함수가 갑자기 수행해야 되는 상황이 발생하면 어떻게 될까?
( blocking system call 은 다음과 같은 상황에서 빠져나온다고 한다.
1. 기능수행성공
2. 기능수행실패
3. 시그널을 받음 )
바로 이점이 내가 몰랐던 부분이다.
그러면, signal handler 에 해당하는 함수를 다 처리하고, 해당 process 는
원래 blocking 상태에 있던 accept 를 수행하던 곳으로 돌아오게 된다.
원래 accept 가 있던 곳에서 새로운 client 의 요청이 있을 때까지 기다리는 상태를 지속하는 것이 아니라, accept 함수는 error 를 의미하는 -1 을 return 한다.
그리고 이 때, errno 를 확인하면,
4번, EINTR [ error : Interrupt ]이 발생한다.
accept 의 man 페이지를 열어서, 언제 EINTR이 발생하는지 보연 아래와 같이 적혀 있다.
The system call was interrupted by a signal that was caught before a valid connection arrived
즉 client 쪽에서 connect 함수를 호출하고, server 쪽에서 blocking 상태에 있던 accept 함수가 동작하는 connection 이 발생하기 이전에, signal 이 발생하여, blocking 상태에 있던 accept 함수의 수행이 중단되었다는 의미이다.
그럼 좀 더 예제의 구체적인 상황을 설명해 보자.
fork 를 이용하여, 프로세스 기반의 다중 접속 서버를 구현할 경우,
parent 프로세스는 accept 를 통해서 client 연결이 인식되면, 해당 client 와의 작업을,
fork 를 이용하여, 별도의 프로세스로 만들어서 처리하게 된다.
그리고 parent 프로세스의 경우, accept 함수를 만나서 또 다른 client 연결 요청을 기다리며, blocking 상태에 있게 된다.
그런데, 이전에 처리되었던 child process 가 작업을 끝내고 종료할 경우,
부모 프로세스는 자식의 프로세스가 종료되었음을 알리는 SIGCHLD 신호를 받게 된다.
SIGCHLD 신호를 받게 되면, accept 에서 block 되어 client 의 연결 요청을 기다리는 일 대신, sigaction 에 등록해 두었던 signal handler 함수가 실행된다.
그리고 이 실행이 끝나면, OS 는 accept 함수의 반환 값으로 -1 을 돌려주고,
errno 에 EINTR 을 적어주게 된다.
그렇기 때문에 parent 프로세스 코드를 짤 때, accept 함수가 -1 을 반환하고,
errno 이 EINTR 인 경우에는, 다시 accept 함수를 실행하여
client의 연결 요청을 기다릴 수 있도록 해주어야 한다.
그래서 code 에는 continue 가 있는 것이다!
원래 기본 코드는
윤성우 님의 "열혈 TCP/IP 소켓 프로그래밍" 의
Chapter10 멀티프로세스 기반의 서버구현 을 가지고 왔다.
윤성우님의 책을 보면서 공부하다가, 내가 실행하면서, 공부를 하다가
나처럼 잘 모르는 사람을 위해서 이런 부분의 설명이 있었으면 더 좋았을텐데...
하는 생각이 들어서 블로그에 정리하게 되었다.
윤성우님의 책에서 필요한 정보를 많이 얻어서 감사하고, 존경한다.
다음 판이 나올 때는 blocking system call 이라는 개념에 대해서도 설명이 함께
있으면 독자들이 더 쉽게 이해할 수 있지 않을까 한다.
원래 코드에서 getpeername, getsockname 등을 이용하여,
연결 발생 시, 서버와 클라이언트의 IP 및 port 정보를 출력하도록 하였다.
그리고 waitpid 의 man 페이지에 나와 있는 sample 코드를 이용하여,
kill -STOP
kill -CONT
등의 신호도 실습할 수 있도록 변경하였다.
그리고, 부모 프로세스에 sigaction 을 이용하여, signal handler 를 등록해 두면,
fork 에 의해 생성된 자식 프로세스에도 동일한 signal handler 가 동작된다는 것을
알게 되었다.
client 와 연결이 있을 때,
kill -CHLD
명령을 이용하여 SIGCHLD 신호를 줄 때와
client 와 연결이 없을 때,
kill -CHLD
명령을 이용하여 SIGCHLD 신호를 줄 때 약간 양상이 다르다.
이는 waitpid 의 기능때문이다.
echo_mpserv.c
echo_clnt.c
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <wait.h>
#include <signal.h>
#define BUF_SIZE 1024
void error_handling( char * msg, int exit_on )
{
fputs( msg , stderr);
fputc( '\n', stderr);
fprintf(stderr, "errno num : %d\n", errno );
fprintf(stderr, "errno msg : %s\n", strerror( errno ) );
if( exit_on )
{
exit(1);
}
}
void proc_child(int sig)
{
pid_t pid;
int status;
pid = waitpid( -1, &status, WUNTRACED | WCONTINUED | WNOHANG );
//pid = waitpid( -1, &status, WNOHANG );
if( pid == -1 )
{
error_handling("waitpid() error!", 0 );
}
printf("=====================\n");
if( WIFEXITED( status ) )
{
printf("child process : %d exit, ret value : %d\n", pid, WEXITSTATUS( status ) );
}
else if( WIFSIGNALED(status) )
{
printf("child process : %d killed by signal %d\n", pid, WTERMSIG(status) );
}
else if( WIFSTOPPED(status) )
{
printf("child process : %d stopped by signal %d\n", pid, WSTOPSIG(status) );
}
else if( WIFCONTINUED(status) )
{
printf("child process : %d continued\n",pid );
}
else
{
printf("Unexpected Flow!\n");
printf("Unexpected Flow!\n");
printf("Unexpected Flow!\n");
}
printf("=====================\n");
}
int main( int argc, char * argv[] )
{
struct sockaddr_in serv_addr;
struct sockaddr_in clnt_addr;
struct sockaddr_in SERV_addr_tmp;
struct sockaddr_in clnt_addr_tmp;
struct sockaddr_in serv_addr_tmp;
int sock_serv;
int sock_clnt;
socklen_t sock_size;
socklen_t sock_size_tmp;
pid_t child_pid;
char buffer[ BUF_SIZE ];
struct sigaction act ;
act.sa_handler = proc_child;
sigemptyset( &act.sa_mask );
act.sa_flags = 0;
sigaction( SIGCHLD, &act, 0 );
if( argc != 2 )
{
error_handling( "[usage] echo_mpserv
}
memset( &serv_addr, 0, sizeof(struct sockaddr_in) );
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = htonl( INADDR_ANY );
serv_addr.sin_port = htons( atoi( argv[1] ) );
sock_serv = socket( PF_INET, SOCK_STREAM ,0 );
if( sock_serv == -1 )
{
error_handling( "socket() fail!", 1 );
}
if( bind( sock_serv, (struct sockaddr *) &serv_addr, sizeof(serv_addr) ) == -1 )
error_handling( "bind() fail!", 1 );
if( listen( sock_serv, 5 ) == -1 )
{
error_handling( "listen() fail", 1 );
}
sock_size_tmp = sizeof( SERV_addr_tmp );
if( getsockname( sock_serv, (struct sockaddr *) &SERV_addr_tmp, &sock_size_tmp) == -1 )
{
error_handling( "getsockname() error!", 1);
}
else
{
printf( "SERVER listening IP : %s, port %u\n",
inet_ntoa( SERV_addr_tmp.sin_addr ),
ntohs( SERV_addr_tmp.sin_port )
);
}
while(1)
{
sock_size = sizeof( clnt_addr );
sock_clnt = accept( sock_serv, (struct sockaddr *) &clnt_addr, &sock_size );
if( sock_clnt == -1 )
{
//error_handling( "accept() fail", 1 );
printf("=====================\n");
error_handling( "accept() fail", 0 );
fprintf(stderr, "child process died?? why INTERRUPT??\n");
printf("=====================\n");
continue;
}
// client IP, port info.
// Later, let's make this as a function!!!
// Later, let's make this as a function!!!
// int getpeername(int s, struct sockaddr *name, socklen_t *namelen);
sock_size_tmp = sizeof( clnt_addr_tmp );
if( getpeername( sock_clnt, (struct sockaddr * ) &clnt_addr_tmp, &sock_size_tmp ) == -1 )
{
error_handling( "getpeername() error!", 1);
}
else
{
printf("=====================\n");
printf( "connected client IP : %s, port %u\n",
inet_ntoa( clnt_addr_tmp.sin_addr ),
ntohs( clnt_addr_tmp.sin_port )
);
}
// Here get server ip, port!!!
//int getsockname(int s, struct sockaddr *name, socklen_t *namelen);
sock_size_tmp = sizeof( serv_addr_tmp );
if( getsockname( sock_clnt, (struct sockaddr *) &serv_addr_tmp, &sock_size_tmp) == -1 )
{
error_handling( "getsockname() error!", 1);
}
else
{
printf( " server IP : %s, port %u\n",
inet_ntoa( serv_addr_tmp.sin_addr ),
ntohs( serv_addr_tmp.sin_port )
);
printf("=====================\n");
}
if( ( child_pid = fork() ) == -1 )
{
error_handling( "fork() fail!", 1 );
}
else if( child_pid == 0 ) // child
{
int read_num;
pid_t child_pid;
child_pid = getpid();
printf("child PID : %u\n", child_pid );
// if you don't close server-socket, check, /fd/
close( sock_serv );
while( ( read_num = read( sock_clnt, buffer, BUF_SIZE ) ) > 0 )
{
if( read_num == -1 )
{
//error_handling( "child read() error!", 1 );
error_handling( "child read() error!", 0 );
return 20;
}
write( sock_clnt, buffer, read_num );
}
close( sock_clnt );
return 0;
}
else // parent
{
close( sock_clnt );
continue;
}
}
close( sock_serv );
return 0;
}
| cs |
echo_clnt.c
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
|
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#define BUF_SIZE 1024
void error_handling( char * msg, int exit_on )
{
fputs( msg , stderr);
fputc( '\n', stderr);
fprintf(stderr, "errno num : %d", errno );
fprintf(stderr, "errno msg : %s", strerror( errno ) );
if( exit_on )
{
exit(1);
}
}
int main( int argc, char * argv[] )
{
struct sockaddr_in serv_addr;
int sock_serv;
char buffer[ BUF_SIZE ];
if( argc != 3 )
{
error_handling( "[usage] echo_clnt
}
memset( &serv_addr, 0, sizeof(struct sockaddr_in) );
serv_addr.sin_family = AF_INET;
serv_addr.sin_addr.s_addr = inet_addr( argv[1] );
serv_addr.sin_port = htons( atoi( argv[2] ) );
sock_serv = socket( PF_INET, SOCK_STREAM ,0 );
if( connect( sock_serv, (struct sockaddr *) &serv_addr, sizeof(serv_addr) ) == -1 )
{
error_handling("connect() fail!", 1);
}
while(1)
{
int write_num;
printf("insert sentence (q/Q to quit): ");
fgets( buffer, BUF_SIZE , stdin );
buffer[ strlen( buffer ) - 1 ] = 0;
if( !strcmp( buffer, "q" ) || !strcmp( buffer, "Q" ) )
{
break;
}
write_num = write( sock_serv, buffer, strlen(buffer) );
read( sock_serv, buffer, strlen(buffer) );
printf("'%s'\n", buffer );
}
close( sock_serv );
return 0;
}
| cs |
아래는 확인을 위하여 도움을 얻은 사이트 및 발췌 내용.
https://kldp.org/node/22102
유닉스/리눅스는 blocking 시스템 호출이 있습니다.
전형적인 예가 select(), accept(), connect(), send(), recv()등인데요.
blocking system call들은 해당기능를 수행하기 까지는 멈춰있다는
의미로 해석해도 무방하겠습니다.
blocking system call은 다음의 상황에서 빠져나오게 됩니다.
1. 기능수행성공
2. 기능수행실패
3. 시그널을 받음
Blocking 의 의미.
https://www.justsoftwaresolutions.co.uk/threading/non_blocking_lock_free_and_wait_free.html
참조.
Definition of Blocking
A function is said to be blocking if it calls an operating system function that waits for an event to occur or a time period to elapse. Whilst a blocking call is waiting the operating system can often remove that thread from the scheduler, so it takes no CPU time until the event has occurred or the time has elapsed. Once the event has occurred then the thread is placed back in the scheduler and can run when allocated a time slice. A thread that is running a blocking call is said to be blocked.
A child created via fork(2) inherits a copy of its parent's signal dispositions. During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.
Return Value
On success, these system calls return a nonnegative integer that is a descriptor for the accepted socket. On error, -1 is returned, and errno is set appropriately.Error handling
Linux accept() (and accept4()) passes already-pending network errors on the new socket as an error code from accept(). This behavior differs from other BSD socket implementations. For reliable operation the application should detect the network errors defined for the protocol after accept() and treat them like EAGAIN by retrying. In the case of TCP/IP, these areENETDOWN, EPROTO, ENOPROTOOPT, EHOSTDOWN, ENONET, EHOSTUNREACH, EOPNOTSUPP, andENETUNREACH.signal 의 man 페이지에서 발췌.
A child created via fork(2) inherits a copy of its parent's signal dispositions. During an execve(2), the dispositions of handled signals are reset to the default; the dispositions of ignored signals are left unchanged.