stdin으로부터 키 입력 체크하기

송지석의 이미지

cygwin환경에서 테스트 프로그램을 짜는 중인데요.
제가 하고 싶은 건
while 루프 안에서 키 입력을 체크하는 루틴을 두고, 키 입력이 있으면 거기에 따른 명령을 실행하는 겁니다.
그런데 키 입력을 받으려고 하니 getc fgetc등은 입력시 엔터를 받을 때 까지 기다리기 때문에 while 루프 안에서 한동안 멈춰있습니다.
키 입력이 있는 지 체크하고 없으면 NULL을 리턴하는 함수 없을까요? 아니면 뭔가 좋은 방법이 있을까요?
이전에 리눅스에서는 while 루프에선 fgets로 입력만 체크하고 thread로 worker를 돌렸었는데 지금은 그럴 필요까지는 없을 것 같고 cygwin에선 될 지 안될 지도 모르고..
뭔가 방법이 없을까요? 간단한 테스트 때문에 두시간 째 삽질중이네요..

File attachments: 
첨부파일 크기
파일 test.cpp959바이트
파일 test.cpp1.05 KB
bugiii의 이미지

C 스트림의 fd를 얻을 수 있나요? 그리고 그 콘솔 fd 도 select 할 수 있나요? 실험해주세요~~~ 궁금합니다... 스티븐 아저씨 책에도 콘솔 입력과 파일 처리를 동시에 select 하는 예제가 있었던 걸로 기억합니다...

DOS 쪽은 kbhit, 윈도우는 Wait* 시리즈중에 Input 하고 같이 하는 것이 있어서 어떻게 될 듯한데요. *nix 환경에서는 어떤지 모르겠습니다.

mach의 이미지

...
fd = fileno(stdin);
....
while(1) {
    select(fd+1, ....);
    ....
   if ( FD_ISSET( fd, &rset)) {
       read( fd, &ch, 1); //
       ...
       switch ( ch ) {
         ...
       }
}

대략 위와같은 형태로 나오겠습니다.
Stevens책에 예제도 있습니다. 정확한 예제는 Unix Network programming
1st edition(현재 3rd까지 나왔지요?) 에서 IO Multiplexing이 나오는 Chapter입니다. 2nd와 3rd Edition에서는 어디쯤 나오는지 가물가물하네요.

------------------ P.S. --------------
지식은 오픈해서 검증받아야 산지식이된다고 동네 아저씨가 그러더라.

bugiii의 이미지

궁금한게 하나 더 있는데요. select 후에 read 를 써야 하나요, fread 를 써야 하나요? 둘중 하나만 일관되게 쓰면 문제가 없나요? mach 님의 경험담을 좀... 책을 찾기가 어려운 상황이라서요... -- 귀찮아서가 아닙니다. -_-;

아... 지금 생각하니까.. 입력이 라인 모드라면? 이라는 생각이 드는군요... 모드를 바꾸는 방법이 있나요? 리모트로 외부에서 라인모드로 접속해서 해당 어플리케이션을 실행한다면 문제가 생기지 않을까요?

jcly2의 이미지

linux에서 작성할 때
termio 구조체랑 tcsetattr 함수를 사용해서
stdin에 변화가 있는지를 조사하구 stdin에 변화가 있으면 값을 읽어 오구
아니면 0 혹은 기타 값을 리턴하게 하면 while안에서도 사용이
가능하더군요..

굳이 select함수를 사용하지 않더라도..

mach의 이미지

bugiii wrote:
궁금한게 하나 더 있는데요. select 후에 read 를 써야 하나요, fread 를 써야 하나요? 둘중 하나만 일관되게 쓰면 문제가 없나요?

버퍼링 vs. formatting 을 누가 하게 할것이냐?라는 전략이 필요한데요.
저는 버퍼링은 제가하는 버릇이 있습니다. 그래서 read를 즐겨씁니다.
때로, fread등의 f*시리즈를 사용하기위해 setbuf()시리즈(setbuf,
setbuffer, setlinebuf, setvbuf)를 써서 버퍼를 조절하고 쓰기도 하지만,
저는 개인적으로는, 가물에 콩나듯합니다. (참, 제가 아는 분중에 후자를
선호하시는 분이 계십니다. )

달리 말씀드리자면 자기자신에게 맞는 방법이 최선책이라고 생각합니다.

bugiii wrote:

... 책을 찾기가 어려운 상황이라서요...

저도 책 찾기 어려운 상황에서 살고 있습니다. 회사에는 책... 없고, 집에도
책꽂이 비숫한(?) 공간에는 "주부생활", "패션잡지"등 이런 것만 꽂을 수 있습니다.
이상하고! 재미없는! 전산관련 책들은 집 창고, 박스속에...... :cry:
단지 최근 몇달간 구입한 책 몇권만 책상위에 있군요.
bugiii wrote:

아... 지금 생각하니까.. 입력이 라인 모드라면? 이라는 생각이 드는군요... 모드를 바꾸는 방법이 있나요? 리모트로 외부에서 라인모드로 접속해서 해당 어플리케이션을 실행한다면 문제가 생기지 않을까요?

Line Discipline (raw, canonical..)관련 말씀을 하시는건지.....
....
어차피 버퍼관리 부터 코딩하자면, 서버가 라인모드를 지원하게 코딩하면 되겠네요.

------------------ P.S. --------------
지식은 오픈해서 검증받아야 산지식이된다고 동네 아저씨가 그러더라.

송지석의 이미지

select를 사용해봤는데
cygwin에서는 타임아웃으로 하면 입력체크가 안되는 것 같습니다.
timeout을 NULL로 하면 입력 체크가 되지만, 이것은 그냥 fgets 하는 것이나 마찬가지고..
왜그런지 모르겠습니다. -_-;;

cococo의 이미지

제대로 작동 안하네요.
원래 의도와는 다르게, 엔터를 치지 않으면, 키를 눌러도 값을 출력 안합니다.
소스에서는 딱히 에러를 못찾겟는데요... -_-;;
누구 에러 보이시는 분.... 지적 부탁드립니다. -_-;;;

행복하시길...
-----------------

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>

int kbhit1( int stdfd );

int main()
{
char c;
int stdfd;
struct termios oldtio, newtio;
stdfd = fileno(stdin);
printf("----\n");

while(1)
{
printf( "%x", c = kbhit1( stdfd ) );
//printf( "%x", c = getchar() );
}

return 0;
}

int kbhit1( int stdfd )
{
fd_set readfs;
struct timeval Timeout;
unsigned char rv;
int res;
while(1)
{
printf(" loop working \n");
FD_SET(stdfd, &readfs);
Timeout.tv_sec = 3;
printf( "select:%d", res = select(stdfd + 1, &readfs, NULL, NULL, &Timeout));
if ( res < 0 )
{ perror(" Error on the select");
return -1; }
else if ( res > 0 )
{ if ( FD_ISSET(stdfd,&readfs) )
{ if ( read(stdfd,&rv,1) < 0 )
{ perror( "Error on read" );
return -1; }
return rv; }
else
return 0;
}
}
}

댓글 첨부 파일: 
첨부파일 크기
파일 0바이트
kslee80의 이미지

stdin 의 fd 값을 얻어서 select 를 하더라도,
stdin 자체가 buffered input 이기 때문에 Enter 키가 입력되기 전에는
select 에서 반응을 보이지 않습니다.
(원래 표준이 이런지, 아니면 구현을 이렇게 해 놓은것인지는 모르겠네요)

DOS 시절 conio.h 에 있던(맞던가..6-.-)
kbhit() 를 원하신다면 이런 코드가 있죠.
(이 코드가 아마 여기(KLDP BBS)에서 본 코드였던걸로 기억합니다 -.-;;)

int kbhit() {
    struct termio t;
    unsigned short flag;
    unsigned char min, time;
    char buf[10];

    ioctl(0, TCGETA, &t);    /* 표준입력 상태파악 */

    flag= t.c_lflag;         /* 값 변경 */
    min= t.c_cc[VMIN];
    time= t.c_cc[VTIME];

    t.c_lflag &= ~ICANON;     /* low 모드로 설정 */
    t.c_cc[VMIN] = 0;         /* read호출시 0개문자 읽어들임 */
    t.c_cc[VTIME]= 0;         /* 시간지연 없음 */

    ioctl(0, TCSETA, &t);     /* 상태변경 */

    if(read(0, buf, 9) <=  0) { /* read호출 */

      t.c_lflag = flag;      /* 원상태로 복구 */
      t.c_cc[VMIN] = min;
      t.c_cc[VTIME]= time;
      ioctl(0, TCSETA, &t);

      return 0;             /*키가 안눌러졌음 */
   }
   else {
      t.c_lflag = flag;     /* 원상태로 복구 */
      t.c_cc[VMIN]= min;
      t.c_cc[VTIME]= time;
      ioctl(0, TCSETA, &t);

      return 1;             /* 키가 눌러졌음을 알림 */
   }
}

헤더는 fcntl.h, sys/ioctl.h, termios.h 입니다.

voider의 이미지

kslee80 님이 말씀하신대로 minicom 과 같은 프로그램에서도 로우 모드로 변경해서 사용하는것 같습니다.

-- 아쉬운 하루 되세요 --

익명 사용자의 이미지

말씀하시는 걸 유닉스 상에서 구현하려면...
유닉스의 기본 프로그램 철학을 약간 벗어 나는 프로그램이 될 것 같습니다. (예를 들어서 다른 프로그램으로 연결한다든 등)

(원격의)터미널에서 보낸 데이터는 커널의 터미널 드라이브가
버퍼링을 하기때문에 엔터를 받기 전에는 어플리케이션까지
데이터를 보내주지않는군요.
-> 즉 아무리 stdin 파일에 nonblocking걸거나 select해봐야
엔터치기 전까지는 커널이 보내주지 않는군요.

해결하려면 데이터직접 터미널에서 데이터를 읽는 방법밖에 없군요.

가장 간단한 방법은

    FILE *inputFile;

    inputFile = fopen("/dev/tty", "r"); /* 에러처리 생략 */
    input = fgetc(inputFile);

이런 방법일 듯한데 시스템에 따라서 tty파일명이 달라지니....
절대 사용해선 안돼는 방법입니다.

위의 예보다는 Unix 의 표준 터미널 제어 함수를 사용해서
non-canonical input mode로 변경하는 방법이 옳은 방법일 것 같네요.

윈도우 방식의 입력을 에뮬하는 라이브러리가 여기 있군요...
http://snap.nlc.dcccd.edu/learn/fuller3/chap5/chap5.html

Detecting Keystrokes로 검색하시면 아마 나올 겁니다.

(-.-;; 확실하지 않은 정보지만... ) 도움이 되시길...

익명 사용자의 이미지

kslee80 wrote:

...
stdin 자체가 buffered input 이기 때문에
...

이 말씀도 좋습니다만, 터미널을 언급할때는,
cannonical mode라고 하는것이 더 어울립니다.

상대적으로 unbuffered보다 raw모드가 어울립니다.

cococo의 이미지

결국 다 잘 안되더군요. -_-;

우선, 문제점 - 엔터를 치기 전에는 /dev/tty로 입력값을 전달 안하는 - 에 대해서는 이해했습니다. (친절한 설명 감사합니다. ㅎㅎㅎ)

에...그래서,

FILE *inputFile;

inputFile = fopen("/dev/tty", "r"); /* 에러처리 생략 */
input = fgetc(inputFile);

를 비롯한 예제의 방법을 모두 시험해 봤습니다만, 안되더군요.

조금 이상한게, [결국 터미널에서 키보드 입력하면, 이 값이 엔터를 누르기 전까진 버퍼에 전달되지 않는다....] 라는 걸 이해하고,
[그렇다면, NonBlocking 모드로 바꾸어보자]라고 생각해서, 답글에 달린 예제 및, 알고 있는 예제를 동원해 봤는데도, 결국 엔터를 누르기 전까지는 값이 안 나가더군요.
------------------------------
-> 즉 아무리 stdin 파일에 nonblocking걸거나 select해봐야
엔터치기 전까지는 커널이 보내주지 않는군요.

해결하려면 직접 터미널에서 데이터를 읽는 방법밖에 없군요.
-------------------------------
이 말대로... -_-;;;;

근데요. 여기 설명에 써 있는 [직접 터미널에서 데이터를 읽는 방법]이 뭐죠?
---------------------------------
가장 간단한 방법은

코드:
FILE *inputFile;

inputFile = fopen("/dev/tty", "r"); /* 에러처리 생략 */
input = fgetc(inputFile);
---------------------------------------
fgetc 와 read는 읽는 버퍼가 서로 다른가요?
즉, read해도 안 되는 이유가 데이터 자체가 버퍼로 전송이 안 되서 그런걸 텐데, 그걸 fgetc로 하면 해결이 된다...라는게 이해가 안 가더라구요.
(사실 테스트 해서 안되는 거 보고 추측한 겁니다. ^^*)

음... 질문이 장황하게 되었네요. 드릴 말씀이...

참고로, 아래가 테스트 한 예제입니다.

/*헤더 파일 및 선언부 생략*/

int main()
{
printf(" %d %d " , sizeof(char), sizeof( char *) );
char c;
int stdfd;
struct termios oldtio, newtio;

stdfd = fileno(stdin);
TTYNonBlockSetting( stdfd , &oldtio, &newtio );
while(1)
{ if ( read( stdfd, &c, 1 ) > 0) printf(" %x\n", c);
// fflush(stdout);
if (c=='z') break;
}
printf(" finish fgetc \n");

return 0;
}

int TTYNonBlockSetting( int stdfd , struct termios *oldtio, struct termios *newtio )
{
unsigned char rv;
tcgetattr( stdfd, oldtio );
*newtio = *oldtio;
newtio->c_lflag = ~ICANON;
newtio->c_lflag = ~ECHO;
newtio->c_cc[VMIN] = 0;
newtio->c_cc[VTIME] = 0;
fcntl( stdfd, F_SETFL, FNDELAY );
tcsetattr( stdfd, TCSANOW, newtio );
// read(stdfd,&rv,1);
// tcsetattr( stdfd, TCSANOW, &oldtio);
return 1;

}

그리고요. 또 하나더 이상한게,
이건 제가 개념이 없는 건지 몰라도....
처음에는

newtio->c_lflag = ~ICANON;
newtio->c_cc[VMIN] = 0;
newtio->c_cc[VTIME] = 0;

이 세줄만으로 Non-Cannonic mode가 적용될 줄 알았는데...

fcntl( stdfd, F_SETFL, FNDELAY );

이걸 코드에 추가 안 해주면, 안 되더라구요?
제가 뭘 잘못 알고 있는건가요?

답글 주신 모든 분 감사드리구요. 행복한 하루 되시기 바랍니다.

댓글 첨부 파일: 
첨부파일 크기
파일 0바이트
moonzoo의 이미지

curses 라이브러리를 쓰시는것도 고려해보세요
(리눅스에서는 ncurses)

wtimeout 함수로 timeout시간을 정해놓고.

밑단에서 wgetch 하시면 됩니다.

키 입력이 바로바로 처리됩니다.

익명 사용자의 이미지

음 오래전 얘기가 리바이벌되었군요.
그때 아예 예제도 하나 올릴것을 그랬나 봅니다.

터미널제어문자열 처리에 대해 조금 공부하시고, telnet등의 터미널제어문자들에 대해 공부해보세요.

그리고, cannonical mode에서 터미널에서 입력된것은 일단 서버컴퓨터로 전송이 됩니다. 서버컴퓨터의 터미널제어루틴에서 유한상태오토마타(FSM)로 코드가 수행됩니다. 버퍼에 넣고, 제어가 끝나기를 기다리게됩니다. 위에 약간 이상한 표현이 있어서 참고삼아 씁니다.

일단, 소스나 올리겠습니다. 질문자님의 소스를 조금 손봐서 올립니다.
프로그램의 종료는 '스페이스바' 즉, 공백문자 되겠습니다.
Raw mode로 진입하게 되어, Ctrl+C도 안먹지요... 당연...

============================

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <termios.h>
#include <string.h>
#include <fcntl.h>
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>

#define BAUDRATE B38400  
int kbhit1( int stdfd );

int main()
{
   char c;
   int stdfd;
   struct termios oldtio, newtio;
   stdfd = fileno(stdin);

//------------------------------
   tcgetattr(stdfd, &oldtio);
   bzero(&newtio, sizeof(newtio));

   newtio.c_cflag = BAUDRATE | CRTSCTS | CS8 | CLOCAL | CREAD;
   newtio.c_iflag = IGNPAR | ICRNL;
   newtio.c_oflag = 0;
   newtio.c_lflag = 0;

   newtio.c_cc[VTIME] = 0;
   newtio.c_cc[VMIN] = 1;

   tcflush(stdfd, TCIFLUSH);
   tcsetattr(stdfd, TCSANOW, &newtio);
//------------------------------

   printf("----\n");

   while(1)
   {
      printf( "0x%x", c = kbhit1( stdfd ) );
      if ( c == 0x20 ) // space
         break;
      //printf( "%x", c = getchar() );
   }
   tcsetattr(stdfd,TCSANOW,&oldtio); // 원래모드로 복귀



   return 0;
}


int kbhit1( int stdfd )
{
   fd_set readfs;
   struct timeval Timeout;
   unsigned char rv;
   int res;
   int cnt;

   while(1)
   {
      printf(" loop working \r\n");
      FD_SET(stdfd, &readfs);
      Timeout.tv_sec = 3;
      printf( "select:%d\r\n", res = select(stdfd + 1, &readfs, NULL, NULL, &Timeout));
      if ( res < 0 )
      { perror(" Error on the select");
         return -1; }
      else if ( res > 0 )
      { if ( FD_ISSET(stdfd,&readfs) )
         { if ( read(stdfd,&rv,1) < 0 )
            { perror( "Error on read" );
               return -1; }
               return rv; }
               else
                  return 0;
      }
   }
}

댓글 달기

Filtered HTML

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

BBCode

  • 텍스트에 BBCode 태그를 사용할 수 있습니다. URL은 자동으로 링크 됩니다.
  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param>
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.

Textile

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • You can use Textile markup to format text.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Markdown

  • 다음 태그를 이용하여 소스 코드 구문 강조를 할 수 있습니다: <code>, <blockcode>, <apache>, <applescript>, <autoconf>, <awk>, <bash>, <c>, <cpp>, <css>, <diff>, <drupal5>, <drupal6>, <gdb>, <html>, <html5>, <java>, <javascript>, <ldif>, <lua>, <make>, <mysql>, <perl>, <perl6>, <php>, <pgsql>, <proftpd>, <python>, <reg>, <spec>, <ruby>. 지원하는 태그 형식: <foo>, [foo].
  • Quick Tips:
    • Two or more spaces at a line's end = Line break
    • Double returns = Paragraph
    • *Single asterisks* or _single underscores_ = Emphasis
    • **Double** or __double__ = Strong
    • This is [a link](http://the.link.example.com "The optional title text")
    For complete details on the Markdown syntax, see the Markdown documentation and Markdown Extra documentation for tables, footnotes, and more.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 사용할 수 있는 HTML 태그: <p><div><span><br><a><em><strong><del><ins><b><i><u><s><pre><code><cite><blockquote><ul><ol><li><dl><dt><dd><table><tr><td><th><thead><tbody><h1><h2><h3><h4><h5><h6><img><embed><object><param><hr>

Plain text

  • HTML 태그를 사용할 수 없습니다.
  • web 주소와/이메일 주소를 클릭할 수 있는 링크로 자동으로 바꿉니다.
  • 줄과 단락은 자동으로 분리됩니다.
댓글 첨부 파일
이 댓글에 이미지나 파일을 업로드 합니다.
파일 크기는 8 MB보다 작아야 합니다.
허용할 파일 형식: txt pdf doc xls gif jpg jpeg mp3 png rar zip.
CAPTCHA
이것은 자동으로 스팸을 올리는 것을 막기 위해서 제공됩니다.