42Seoul [minitalk]
minitalk
이 과제는 UNIX signal을 이용한 소규모 데이터 교환 프로그램을 작성하는 데에 있습니다..
Norminette
기준에 맞춰서 작성되어야 한다.- 프로그램이 예기치 않게 종료되면 안된다.
- 메모리 누수는 없어야 한다.
Makefile
은 -Wall -Wextra and -Werror 플래그로 작성되어 컴파일되어야 하고 리링크되지 않아야 한다.- $(NAME), all, clean, fclean, re 룰을 포함해야 한다.
- 실행파일은 각각 client와 server로 이름지어야 한다.
사용 가능한 함수 |
---|
write |
signal |
sigemptyset |
sigaddset |
sigaction |
kill |
getpid |
malloc |
free |
pause |
sleep |
usleep |
exit |
필수 파트
- 클라이언트와 서버가 서로 통신하는 프로그램을 작성해야 한다.
- 서버와 클라이언트 중 서버가 먼저 실행되야하며, 실행 후 PID값을 표시해야한다.
- 클라이언트가 실행될 때 다음의 매개변수를 받는다.
- 서버 PID
- 전송할 문자열
- 클라이언트는 매개변수로 전달한 문자열을 서버로 통신해야 하며 서버는 해당 문자열을 표시해야 한다.
- 작성한 클라이언트, 서버는 오직 UNIX signal을 이용해야 한다.
- 서버는 문자열을 매우 빠른 속도로 표시할 수 있어야 한다. 100개의 문자로 이루어진 문자열을 표시하는데 1초가 걸린다면 매우 오래걸린 것
- 서버가 재시작할 필요없이 여러 클라이언트로 부터 문자열을 연속으로 수신할 수 있어야 한다.
- SIGUSR1과 SIGUSR2 두 신호만 사용할 수 있습니다.
보너스 파트
소규모 수신확인 시스템을 추가하고 유니코드 문자도 지원하게 하는것
시작하기 전 필요한 정보들
평가때 많이 보고 시작하기전 선수지식에 대한 정보를 들어서 쉽게 정리하며 진행했다.
프로세스
프로세스라는 개념을 명확하게 하는것이 매우 중요하다.
어떠한 프로그램이 돌아가기위해선 프로세스가 동작해야한다.
그렇다고 해서 프로그램과 프로세스가 같은 개념은 절대 아니다.
윈도우나 맥의 현재 상태를 찍어보면 얼마나 많은 프로세스들이 돌아가고 있는지 확인이 가능하다.
우리가 한쪽에는 유튜브를 틀어놓고 게임이 가능한 이유는 멀티태스킹이 가능하기 때문인데 이는 운영체제가 여러개의 프로세스를 동작하고 있기 때문이다.
이러한 프로세스안에서도 여러가지 동작을 동시에 수행해야 하기 때문에 Thread
가 동작한다.
리눅스 관점에서 쉽게 풀어서 말한다면 ls
명령어 프로그램은 대부분 bin/ls
로 존재한다.
이러한 프로그램을 여러 사용자가 동시에 사용하게 된다면 ls
프로세스가 여러개 생성되는 것이다.
실제로 ps
명령어를 사용해보면 ps
프로그램의 프로세스가 잡히는것을 확인할 수 있다.
정리하자면 프로그램이 실행중인 상태 또는 환경을 의미한다.
쓰레드는 일꾼으로 많이 부름
PID
Process ID의 준말로 프로세스를 식별하기 위해 부여하는 번호이다.
PPID
해당 프로세스를 만든 프로세스로 부모 프로세스 PID이다.
시그널
시그널은 과제에서는 유닉스 시그널에 대해서 언급하지만 리눅스/유닉스에 한정되지 않고 운영체제에 보편적으로 있는 개념자체이다.
특정 이벤트가 발생하였을 때 신호를 보내서 알려주는 것을 말하는데 정말 간단하게 무한루프 프로그램을 종료하기 위해서 Ctrl + C
를 누르는 행위또한 시그널의 일종이다.
시그널의 종류는 매우 다양하며 kill -l
명령어를 통해 확인할 수 있다.
HUP INT QUIT ILL TRAP ABRT EMT FPE KILL BUS SEGV SYS PIPE ALRM TERM URG STOP TSTP CONT CHLD TTIN TTOU IO XCPU XFSZ VTALRM PROF WINCH INFO USR1 USR2
위에서 프로세스에 대해서 알았으니 시그널을 통해 통신을 해야한다.
하지만 프로세스는 부모와 자식간이라도 완전히 단절되어 있다.
시그널
을 통해하여 간단한 신호를 줄 수 있다.
프로세스가 시그널을 받게 되면 시그널에 해당되는 동작을 하거나 무시하거나 사용자 정의함수로 동작방식을 변경할 수 있다.
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void Handler(int sig)
{
printf("시그널을 받았습니다. 프로그램을 종료합니다.\n 시그널 번호 %d", sig);
sleep(2);
exit(0);
}
int main()
{
signal(SIGINT, Handler);
printf("Ctrl+C를 입력하세요\n");
while(1);
}
시그널을 이해하기 좋은 예제로 ctrl + c
로 SIGINT신호를 보낸것이다.
signal함수의 sig인자로 SIGSTOP
을 주게 되면 바로 멈춰버린다.
즉 핸들러함수가 실행되지 않고 프로세스가 강제종료된다.
이번 과제에서는 시그널 전달변수 SIGUSR1
, SIGUSR2
로 데이터를 전달한다.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<signal.h>
void sig_usrtest(int sig)
{
if(sig == SIGUSR1)
printf("received SIGUSR1\n");
else if(sig == SIGUSR2)
printf("received SIGUSR2\n");
}
int main(void)
{
if(signal(SIGUSR1, sig_usrtest) == SIG_ERR)
perror("에러입니다.\n");
if(signal(SIGUSR2, sig_usrtest) == SIG_ERR)
perror("에러입니다.\n");
while(1)
{
printf("대기중..\n");
sleep(60);
}
}
간단한 시그널 통신 예제이다.
한쪽에서 해당 프로그램을 실행시키고 다른 탭으로 터미널을 실행시켜 ps
로 해당 프로세스 id를 알아내고 kill -USR1 PID
를 통해 실행 터미널로 시그널을 줄 수 있다.
비트마스크
비트마스크란, bit를 활용한 테크닉으로 컴퓨터에 사용되는 최소 데이터 단위를 다루는 것이다.
0/1의 상태만 가질 수 있기 때문에 이진법을 사용하여 나타낸다. on/off true/false
이러한 이진수는 10진수나 데이터형태로 변경이 가능하기 때문에 (아스키 코드 등등) 비트마스크를 할줄 안다면 데이터를 구성할 수 있다.
기본적인 비트 연산
AND 연산 &
&
와 &&
전혀 다르다 전자는 비트 연산자이고 후자는 조건문에서 많이 사용하는 AND연산자이다.
1010 & 1111 = 1010
// 대응하는 두 비트가 참(1)일 경우에만 참(1)을 반환
OR 연산 ** **
마찬가지로 |
와||
은 전혀 다르다.
1010 | 1111 = 1111
// 대응하는 두 비트가 하나라도 참(1)일 경우 참(1)을 반환
XOR 연산 ^
배타적 연산으로 불린다.
1010 ^ 1111 = 0101
// 대응하는 두 비트가 서로 다르면 참(1)을 반환
NOT 연산 ~
~1010 = 0101
// 비트의 값을 반전하여 반환
Shift연산
>>
,>>
00001010 << 2 = 00101000
00101000 >> 3 = 00000101
// 비트의 값을 이동하여 반환
해당 꺽쇠 방향으로 숫자만큼 비트가 이동한다고 생각하면 된다.
남은 비트는 0
으로 채워진다.
계산시 거듭제곱의 계산이 되기 때문에 (10진수 변환 시)사칙연산보다 용이할 수 있다.
실제로 그래픽의 연산은 비트연산으로 이루어진다.
사용가능한 함수
처음보는 함수들이 많아서 직접 사용해보면서 공부한다.
write
#include<unistd.h>
ssize_t write(int fd, const *buf, size_t bufsize);
앞서 많은 과제에서 사용했지만 fd로 지정한 파일 디스크럽터의 스트림에 해당 버퍼 그리고 사이즈만큼을 쓴다.
이번과제에서는 sever에 출력할 때 사용. (기본 입출력 함수)
malloc
#include <stdlib.h>
void* malloc(size_t size)
메모리 관리를 위한 함수로 동적할당 함수이다.
free
#include <stdlib.h>
void free(void* ptr)
마찬가지로 메모리관리를 위한 함수로 할당된 메모리를 해제하는 함수이다.
sleep
#include <unistd.h>
unsigned int sleep(unsigned int seconds);
스레드의 실행을 초 단위로 일시 중단하는 함수이다.
usleep
#include <unistd.h>
int usleep(useconds_t microseconds);
sleep과 다르게 마이크로초를 인자로 받기 때문에 세밀한 시간계산에 사용된다.
구식이라 nanosleep()을 권장한다.
exit
#include <stdlib.h>
void exit(int status);
이 함수는 프로세스를 종료하는 함수이다.
함수안에서 돌아가는 과정은 다음과 같지만 아직은 크게 뭔지 모르겠다.
- atexit(3) 기능에 등록된 기능을 등록의 역순으로 호출합니다.
- 열려 있는 모든 출력 스트림을 플러시합니다.
- 열려 있는 스트림을 모두 닫습니다.
- tmpfile(3) 기능으로 작성된 모든 파일의 연결을 해제합니다.
getpid
#include <unistd.h>
pid_t getpid(void);
시스템 콜 함수로 호출 프로세스의 ID를 반환한다.
kill
#include <signal.h>
int kill(pid_t pid, int sig);
시스템 콜 함수로 지정된 pid값으로 해당 sig를 보낸다.
pause
#include <unistd.h>
int pause(void);
일시중지함수로 시그널이 수신될 때 까지 호출 스레드를 일시 중지한다.
signal
#include <signal.h>
void (*signal(int signum, void (*handler)(int)))(int);
signum은 시그널을 발생시키는 번호로 매크로를 쓸 수 있다.
handler는 함수포인터로 함수를 인자로 주게 되면 시그널을 받았을 때 해당 함수가 실행된다.
실행 방법 정리 ->
sigaction
#include <signal.h>
int sigaction(int sig, const struct sigaction *restrict act, struct sigaction *restrict oact);
signal함수보다 향상된 기능을 제공하는 시그널 함수이다.
signal이 해당 시그널에 대해서 특정 함수를 지정한다면 sigaction은 특정 시그널에 sigaction구조체를 지정한다.
sigaction
struct sigaction {
union __sigaction_u __sigaction_u; /* signal handler */
sigset_t sa_mask; /* signal mask to apply */
int sa_flags; /* see signal options below */
};
union __sigaction_u {
void (*__sa_handler)(int);
void (*__sa_sigaction)(int, siginfo_t *, void *);
};
처음보면 구조가 복잡하다고 생각할 수 있지만 간단하게 sigation구조체를 본다면 signal함수와 같이 특정 시그널에 함수를 등록해서 사용한다.
sigaction을 사용하는 이유는 server가 client의 pid값을 알아야하기 때문에 해당 pid값을 담고 있는 siginfo_t
가 필요하다.
siginfo_t {
int si_signo;
int si_errno;
int si_code;
pid_t si_pid;
uid_t si_uid;
int si_status;
clock_t si_utime;
clock_t si_stime;
sigval_t si_value; //시그널 값
int si_int;
void * si_ptr;
void * si_addr;
int si_band;
int si_fd;
}
해당 함수로 호출하기 위해서 sa_flag
의 값이 SIGINFO를 사용하면 sa_sigaction의 시그널 함수를 사용하고 사용하지 않는 경우는 sa_handler로 사용되게 된다.
따라서 이 과제의 보너스 파트를 위해 sa_flag
를 사용해서 siginfo_t
값을
sigaddset
#include <signal.h>
int sigaddset(sigset_t *set, int signo);
시그널을 추가할 수 있는 함수이다.
sigemptyset
#include <signal.h>
int sigemptyset(sigset_t *set);
풀이 방법
간단하게 kill함수로 특정 pid에 시그널을 보낼 수 있고 signal함수로 시그널 인풋에 대해서 함수를 설정할 수 있다는 점.
값 계산은 위 처럼 비트마스크를 사용하면 간단하게 해결 가능하다.
보너스를 위해서 sigaction함수를 사용하여 클라이언트 pid를 전달하여 출력한다.
🎵🎧
댓글남기기