-
[시스템프로그래밍] User Program시스템프로그래밍 2023. 12. 12. 20:30
https://scwcart.tistory.com/entry/Termianal-Driver
Termianal Driver
이번 글에서는 터미널 드라이버에 대해 알아보겠습니다. 터미널 드라이버를 다루기 전에, 간단히 파일에 대해 짚고 넘어가 보도록 하겠습니다. Files 리눅스/유닉스 시스템 안의 모든 것은 파일
scwcart.tistory.com
이전 글에서 터미널 드라이버에 대해서 알아봤으니
이제 터미널로 들어오는 사용자 입력에 따라 다르게 동작하는 프로그램을 만들어 보도록 하겠습니다.
Writing a User Program
다음과 같은 로직으로 작동하는 play_again 프로그램을 작성해 보겠습니다.
1. 먼저 프롬프트로 사용자에게 프로그램을 실행할 건지 물어봅니다.
2. 사용자에게 yes/no 중 하나의 대답을 받습니다.
3. 만약 yes라고 하면 0을 반환하고,
4. no라고 하거나 EOF(Ctrl+D)를 입력하면 1을 반환합니다.
#include <stdio.h> #include <termios.h> #define QUESTION "Do you want another transaction" int get_response( char * ); int main() { int response; response = get_response(QUESTION); return response; } int get_response(char *question) { printf("%s (y/n)? ", question); while(1) { switch( getchar() ) { case 'y': case 'Y': return 0; case 'n': case 'N': case EOF : return 1; } } }
위 로직대로 작성한 프로그램입니다.
실행하면 이런 결과가 나옵니다. yes나 no 대신 다른 입력이 들어오면 무시하고 yes/no 중 다른 대답이 올 때까지 기다립니다.
이 프로그램에는 두 가지 문제가 있습니다.
1) 사용자가 엔터를 눌러야만 프로그램이 입력에 따라 작동할 수 있습니다.
2) 유저가 엔터를 누르면 프로그램은 입력 라인 한 줄 전체를 다 받습니다. 즉, 프로세스는 사실 'y'나 'n'이라는 알파벳 하나만 필요한데 사용자가 'yes'나 'no', 혹은 좀 더 극단적인 예를 들자면 'yes I love it!' 이런 식으로 문장을 입력한다면 터미널 드라이버는 문장 전체를 다 전달하겠죠.
두 가지 문제 모두 입력이 canonical mode에서 동작해서 발생하는 문제입니다. 그럼 프로그램이 canonical input mode가 꺼진 상태에서 작동할 수 있도록 프로그램을 다시 작성해 보도록 하겠습니다.
Retrieving and Modifying Terminal Attributes
명령어 입력이 아니라 프로그램 자체에서 터미널 제어 속성을 검색 및 수정하기 위해서는
tcgetattr(), tcsetattr() 함수를 사용할 수 있습니다.
이름처럼 전자가 검색, 후자가 수정을 위한 함수겠죠?
#include <termios.h> int tcgetattr(int fd, struct termios *termios_p); int tcsetattr(int fd, int optional_actions, const struct termios *termios_p);
함수 형태는 이렇습니다.
두 함수 모두 성공 시 0, 실패 시 -1을 반환합니다.
인수 중 fd는 터미널을 가리키는 파일 디스크립터입니다. fd가 터미널을 참조하지 못하면 ENOTTY 오류를 발생시킵니다. *termios_p 인수는 termios 구조체를 가리키는 포인터입니다. 그럼 termios 구조체가 무엇일까요?
termios 구조체는 터미널의 모든 입출력 속성들을 기록하 구조체입니다.
struct termios { tcflag_t c_iflag; //Input flag tcflag_t c_oflag; //Output flag tcflag_t c_cflag; //Control flag tcflag_t c_lflag; //Local modes cc_t c_line; //Line discipline(nonstandard) cc_t c_cc[NCCS]; //Terminal special characters speed_t c_ispeed; //Input speed(nonstandard; unused) speed_t c_ospeed; //Output speed(nonstandard; unused) };
구조체 구성은 이렇습니다.
1) 위쪽 4개의 필드는 모두 해당 모드를 위한 플래그들을 정하는 비트마스크입니다.
각 모드는 코드에 쓰여진 대로 iflag는 입력, oflag는 출력, cflag는 제어, lfag는 로컬 모드를 제어하는 플래그입니다.
제어 모드의 경우 터미널 하드웨어 제어와 관련된 모드이고, 로컬 모드는 터미널 입력을 위한 사용자 인터페이스를 관리합니다.
2) c_line은 터미널에 대한 라인 규칙을 명시하는 필드입니다.
라인 규칙은 일반적으로 canonical mode I/O를 구현한 N_TTY 로 설정되어 있습니다.
3) c_cc 배열은 interrupt, suspend 등을 의미하는 터미널 특수 문자들과
noncanonical mode 작동을 제어하는 필드들을 포함합니다.
4) c_ispeed, c_ospeed는 리눅스에서는 사용되지 않습니다.
터미널 플래그들은 커맨드라인 입력이나 위에서 언급한 tcsetattr() 함수로 수정할 수 있습니다.
커맨드라인 입력으로 수정하는 건 이전 글에서 해봤던 건데요,
stty icanon을 하면 canonical mode가 켜지고, 반대로 stty -icanon을 하면 꺼집니다.
echo도 유사하게 stty echo 하면 켜지고, stty -echo 하면 꺼집니다.
그런데 이렇게 커맨드라인 입력으로만 입력을 제어하면 입력 제어를 모두 사용자에게 맡기는 것이 되어버리므로
프로그램에서 입력 속성을 미리 정해줄 필요가 있겠죠?
터미널 플래그들은 전부 비트마스크로 되어 있으므로 &= ~ICANON 과 같이 비트 정보롤 변경하여 껐다 켰다 할 수 있습니다.
인수 중 optional_actions는 이미 큐에 저장되어 있는 입출력 속성을 어떻게 취급할 것인지 정하는 것으로 다음 값들 중 하나를 사용할 수 있습니다.
1) TCSANOW: 즉시 속성 변경
2) TCSADRAIN: 큐에 저장된 출력이 터미널에 쓰여질 때까지 기다린 후에 속성 변경. 즉 변경한 파라미터가 출력에 영향을 미칠 때 사용하는 옵션
3) TCSAFLUSH: TCSADRAIN과 똑같이 동작하되, 큐에 저장된 입력을 무효화
4) TCSASOFT: 터미널 하드웨어에 대한 상황의 변경을 금지하기 위한 것으로, 위의 3가지 옵션들과 덧붙여 사용 가능
그럼 termios 구조체를 활용하여 프로그램 상에서 입출력 속성을 제어해 보도록 하겠습니다.
#include <stdio.h> #include <termios.h> #define QUESTION "Do you want another transaction" int get_response( char * ); int main() { int response; tty_mode(0); //save tty mode(입력 모드) set_crmode(); //set char-by-char mode response = get_response(QUESTION); tty_mode(1); //restore tty mode return response; } int get_response(char *question) { printf("%s (y/n)? ", question); while(1) { switch( getchar() ) { case 'y': case 'Y': return 0; case 'n': case 'N': case EOF : return 1; default: printf("\ncannot understand %c, ", input); printf("Please type y or no\n"); } } } void set_crmode() // purpose: fd 0 (= stdin)이 입력을 한글자씩 받도록(noncanonical) // method: use bits in termios { struct termios ttystate; tcgetattr(0, &ttystate); ttystate.c_lflag &= ~ICANON; // lflag: local. 터미널 입력에서 canonical 끄기 ttystate.c_cc[VMIN] = 1; // get 1 char at a time(한 번에 한 글자씩) tcsetattr(0, TCSANOW, &ttystate); // stdin에 바꾼 ttystate를 즉시 적용(TCSANOW) } int tty_mode(int how) // how: 0 -> save current mode, 1 -> restore mode { static struct termios original_mode; if (how == 0) tcgetattr(0, &original_mode); else return tcsetattr(0, TCSANOW, &original_mode); }
주석으로 설명이 달린 부분이 play_again0에서 달라진 부분입니다.
처음 tty_mode() 함수에서 tcgetattr()로 현재 입력 모드를 저장합니다.
그리고 set_crmode()에서 플래그에 비트 연산을 하여 canonical mode를 끄고,
c_cc배열의 VMIN 부분에 1을 저장하여 한 번에 한 글자씩 받아오도록 설정합니다.
그런 뒤 tcsetattr()로 해당 정보를 저장하고, 다시 tty_mode()함수에서 바뀐 모드로 restore합니다.
프로그램을 실행해 y나 n이 아닌 알파벳을 한 글자 한 글자 입력할 때마다 다시 입력하라는 안내문이 출력되고,
y를 입력하자마자 바로 프로그램이 종료되는 것을 볼 수 있습니다.
play_again0에서는 yes 전체를 다 입력하고 엔터를 누를 때까지 프로그램이 종료되지 않았던 것과 비교하면
입력 모드가 달라졌음을 알 수 있습니다.
ECHO
지금 만든 play_again1에서 한 가지 아쉬운 점이 있다면,
사용자가 y가 n이 아닌 다른 글자를 입력했을 때는 굳이 터미널에 출력되지 않게 하고 싶다는 것입니다.
방법은 간단합니다.
그냥 echo를 끄면 됩니다.
y나 n이 아닌 경우 전부 무시하므로 에러 메시지도 당연히 출력되지 않겠죠?
play_again1의 set_crmode() 함수에서
ttystate_lflag &= ~ECHO; 만 추가한 뒤 실행시킨 모습입니다.
다른 글자를 입력할 땐 아무 반응도 없다가 y가 들어오면 바로 종료됩니다.
그러나 여기서 또 다른 문제가 발생합니다. y나 n이 들어올 때까지는 프로그램이 종료되지 않는다는 것입니다.만약 이 프로그램이 실제 ATM기에서 사용된다고 가정했을 때, A 고객이 y나 n을 누르지 않고 떠난다면 그 다음에 도착한 B 고객은 A 고객의 계좌에 접근할 수 있을 것입니다. 그러면 정말 위험하겠죠? 그래서 우리는 일정 시간이 지나면 프로그램이 자동 종료하도록 타임아웃을 설정해 주어야 합니다.
Blocking mode
리눅스에서 read(),write() 함수는 입출력이 blocking인지, non-blocking인지에 따라 작동방식이 조금 다릅니다.
blocking일 경우 사용자의 입력을 받기 위해 read에서 기다릴 수 있습니다.
즉, 사용자가 write하기 전에는 read에서 빠져 나오지 못하도록 커널이 데이터가 들어올 때까지 봉쇄(blocking)시킵니다.
데이터가 들어오면 read함수는 return되고, 다시 프로그램으로 돌아오게 됩니다.
non-blocking은 시스템 함수 호출 후 멈출 필요 없이, 읽을 데이터가 있으면 읽고, 없으면 넘어가는 방식입니다.
프로그램에서 read를 호출할 때, 커널은 Loop를 돌면서 계속 읽을 데이터가 있는지 없는지 검사하고, 없으면 리턴합니다.
우리는 사용자 입력이 없으면 빨리 종료하고 넘어가야 하므로 non-blocking 방식을 선택해야겠죠?
Blocking이란 터미널에 대한 연결(파일 디스크립터)뿐만 아니라 열려 있는 모든 파일에 대한 속성입니다.
따라서 open()이나 fcntl()에서 non-blocking 모드를 설정해 주면 되는데,
리눅스에는 이에 해당하는 상수로 O_NDELLAY가 정의되어 있습니다. 이름은 조금 다르지만, O_NONBLOCK과 동의어입니다.
그럼 non-block 모드로 설정한 후 입력을 찾지 못하면 2초 정도 sleep한 뒤 다시 입력을 찾고,
3번 안에 입력을 찾지 못하면 종료하도록 다시 프로그램을 짜 보겠습니다.
#include <stdio.h> #include <termios.h> #include <fcntl.h> #include <string.h> #include <unistd.h> #include <ctype.h> #define ASK "Do you want another transaction" #define TRIES 3 #define SLEEPTIME 2 #define BEEP putchar('\a') int get_response( char *, int ); int get_ok_char(); void set_cr_noecho_mode(); void set_nodelay_mode(); void tty_mode(int ); int main() { int response; tty_mode(0); set_cr_noecho_mode(); //set char-by-char and noecho mode set_nodelay_mode(); response = get_response(ASK, TRIES); tty_mode(1); return response; } int get_response(char *question, int maxtries) { int input; printf("%s (y/n)? ", question); fflush(stdout); while(1) { sleep(SLEEPTIME); input = tolower(get_ok_char()); if( input == 'y') return 0; if (input == 'n') return 1; if (maxtries-- == 0) //3번 시도 끝에 못 찾으면 종료 return 2; BEEP; } } void set_cr_noecho_mode() { struct termios ttystate; tcgetattr(0, &ttystate); ttystate.c_lflag &= ~ICANON; ttystate.c_lflag &- ~ECHO; //no echo ttystate.c_cc[VMIN] = 1; tcsetattr(0, TCSANOW, &ttystate); } void set_nodelay_mode() // purpose: stdin(fd 0)을 non-blocking mode(=no-delay mode)로 // method: use fcntl to set bits { int termflags; termflags = fcntl(0, F_GETFL); //current setting 읽어오기 termflags |= O_NDELAY; //nondelay bit 뒤집기 fcntl(0, F_SETFL, termflags); //바뀐 flag 상태 저장하기 } void tty_mode(int how) // termios, fcntl flag 둘다 저장 { static struct termios original_mode; static int original_flags; if (how == 0) { tcgetattr(0, &original_mode); original_flags = fcntl(0, F_GETFL); } else { tcsetattr(0, TCSANOW, &original_mode); fcntl(0, F_SETFL, original_flags); } } int get_ok_char() { int c; // EOF나 y,n 만날 때까지 다른 입력은 무시하고 반복 while( ( c=getchar() ) != EOF && strchr("yYnN", c) == NULL ) ; return c; }
set_nodelay_mode()에서 fcntl()를 이용해 O_NDELAY 비트를 바꿔 주고,
tty_mode()dp fcntl flag 정보를 저장하는 코드를 추가합니다.
실행해 보면, y나 n이 입력되지 않았는데도 자동으로 종료되는 모습을 볼 수 있습니다.
끝!
'시스템프로그래밍' 카테고리의 다른 글
[시스템프로그래밍] Process Management (0) 2023.12.13 [시스템프로그래밍] Computer Memory and Program (0) 2023.12.13 [시스템프로그래밍] Process (0) 2023.12.13 [시스템프로그래밍] Signals (0) 2023.12.12 [시스템프로그래밍] Termianal Driver (0) 2023.12.12