Pseudo Terminal (유사? 터미널)

본 포스트는 APUE chap.19 을 참고하여 작성함.

Pseudo Terminal은 application 이 보기에는 터미널로 보이지만, 사실 진짜 터미널이 아닌 것이다.
Pseudo Terminal의 구조, 예시, 사용법에 대해 설명하도록 함.

Pseudo Terminal 개요

Pseudo terminal 을 사용하는 프로세스들의 전형적인 구조는 아래와 같다.

19.1

  • 부모 프로세스가 pseudo terminal master를 open 하고 fork -> 자식 프로세스는 session을 새로 만듬 -> pseudo terminal slave를 open -> standard input, output, error를 pseudo terminal에 연결 -> exec 실행
  • Pseudo terminal slave에 연결된 자식 프로세스는 real terminal에 연결된거처럼 terminal I/O function을 사용할 수 있음
  • 부모 프로세스의 output은 자식 프로세스는 input, 자식 프로세스의 output은 부모 프로세스의 input

이렇게만보면 부모가 뭔지, 자식이 뭔지 잘 이해가 안가니 예시를 보자.

1. Network Login Servers

19.3

  • 원격 호스트에서 로그인 쉘이 실행되면 위와 같은 구조가 됨
  • exec가 두번인 이유는 중간에 login 프로세스가 있어서...
  • rlogind는 I/O 멀티플레싱 필요로 함.
    • rlogin 서버는 다른 입출력 스트림(TCP/IP)도 함께 읽고 씀
    • rlogin이 어떤식으로든 select나 poll 같은 입출력 다중화 사용 혹은 두 프로세스 or 스레드로 나눠져야 함을 의미

2. Windowing System Terminal Emulation

19.4

  • 터미널 에뮬레이터는 shell과 Window manager사이에서 중재자 역할로, 각 shell은 각자의 window에서 실행됨
  • 터미널 에뮬레이터 창의 크기를 바꾸게 되면, 터미널 에뮬레이터에 알려주고 PTY master에 ioctl의 TIOCSWINSZ 명령을 실행해 부장치쪽 창 크기 변경
    • ioctl(fd, termios.TIOCSWINSZ, struct.windowsize)
    • 이때 입력받은 크기가 다르면 커널은 SIGWINCH 신호를 PTY slave로 보냄
      화면을 다시 그려야 하는 응용프로그램은 해당 SIGWINCH를 캐치해 ioctl의 TIOCSWINSZ 명령을 통해 새 크기를 얻은 후 화면을 다시 그림

3. Script Program

대부분 Unix 시스템에서 제공되며 터미널 세션 도중의 모든 input, output를 file에 저장하는 프로그램
19.5

  • shell과 터미널 사이에 자신을 끼워 넣음
  • script 실행중에는 PTY slave위에 terminal line discipline에서 나오는 모든 내용을 스크립트 파일에 복사 (typescript)
    • 사용자가 누른 키들 역시 terminal line discipline을 거처 echo -> 스크립트 파일에 사용자 입력도 기록
    • password는 echo되지 않으므로 스크립트 파일에 패스워드기록은 없음

그 외 책에는 몇가지 예시가 더 있는데, 생략함.

Pseudo Terminal 열기

PTY Master Open

#include <stdlib.h>
#include <fcntl.h>

int posix_openpt(int oflag);
  Returns: file descriptor of next available PTY master if OK, −1 on error
  • oflag : open함수에 사용되는 bitmask
    • O_RDWR : readable-and-writable
    • O_NOCTTY : 컨트롤링 터미널이 되는것을 방지

PTY Slave 권한 변경 함수

#include <stdlib.h>
int grantpt(int fd);
int unlockpt(int fd);
  Both return: 0 on success, −1 on error
  • fd : master fd
  • grantpt(fd)
    • slave 노드의 user ID를 호출자의 real user ID로 설정, 노드 그룹ID를 unspecified 값으로 설정
    • permission은 읽기 및 쓰기를 허용하고 그룹소유자의 쓰기를 허용하는 접근권한설정
  • unlockpt 함수는 pseudo-terminal의 slave 에 대한 접근을 허용하는데 사용

PTY Slave 이름을 가져오는 함수

#include <stdlib.h>
char *ptsname(int fd);
  Returns: pointer to name of PTY slave if OK, NULL on error
  • fd : master fd
  • 해당 fd에 대응되는 slave 의 경로 이름을 돌려줌

위 함수를 편하게 사용하게 APUE에서 만든 함수

#include "apue.h"
int ptym_open(char *pts_name, int pts_namesz);
  Returns: file descriptor of PTY master if OK, −1 on error
int ptys_open(char *pts_name);
  Returns: file descriptor of PTY slave if OK, −1 on error
  • ptym_open : 사용 가능한 pty master open, 호출 성공 시 대응되는 slave 이름을 pts_name을 통해 알려줌
  • ptys_open : ptym_open 이후 얻어진 pts_name을 통해 호출하면 slave 생성
    • ptym_open 함수가 버퍼보다 긴 문자열을 복사하는 일이 없도록 하기 위해, 호출자는 버퍼의 길이를 pts_namesz 인수로 제공해야 함

구현체

  • ptym_open 함수를 이용해 pty master를 찾아 열고 얻어진 pts_name을 통해 ptys_open으로 pty slave를 오픈
#include "apue.h"
#include <errno.h>
#include <fcntl.h>
#if defined(SOLARIS)
#include <stropts.h>
#endif
 
int ptym_open(char *pts_name, int pts_namesz)
{
  char *ptr;
  int fdm, err;
  if ((fdm = posix_openpt(O_RDWR)) < 0)
    return(-1);
  if (grantpt(fdm) < 0) /* grant access to slave */
    goto errout;
  if (unlockpt(fdm) < 0) /* clear slave’s lock flag */
    goto errout;
  if ((ptr = ptsname(fdm)) == NULL) /* get slave’s name */
    goto errout;
 
  /*
  * Return name of slave. Null terminate to handle
  * case where strlen(ptr) > pts_namesz.
  */
  strncpy(pts_name, ptr, pts_namesz);
  pts_name[pts_namesz - 1] = ’\0’;
  return(fdm); /* return fd of master */
 
errout:
  err = errno;
  close(fdm);
  errno = err;
  return(-1);
}
 
int ptys_open(char *pts_name)
{
  int fds;
 
#if defined(SOLARIS)
  int err, setup;
#endif
 
  if ((fds = open(pts_name, O_RDWR)) < 0)
    return(-1);
 
#if defined(SOLARIS)
/*
* Check if stream is already set up by autopush facility.
*/
  if ((setup = ioctl(fds, I_FIND, "ldterm")) < 0)
    goto errout;
  if (setup == 0) {
    if (ioctl(fds, I_PUSH, "ptem") < 0)
      goto errout;
    if (ioctl(fds, I_PUSH, "ldterm") < 0)
      goto errout;
 
    if (ioctl(fds, I_PUSH, "ttcompat") < 0) {
errout:
      err = errno;
      close(fds);
      errno = err;
      return(-1);
    }
  }
#endif
  return(fds);
}
pty_fork Function

fork를 통해 자식 프로세스를 만들고 앞의 두 함수를 이용하여 자식 프로세스는 session leader이며 controlling terminal를 가지게 하는 함수

#include <termios.h>
pid_t pty_fork(int *ptrfdm, char *slave_name, int slave_namesz,
               const struct termios *slave_termios,
               const struct winsize *slave_winsize);
  Returns: 0 in child, process ID of child in parent, −1 on error
  • ptrfdm : PTY master의 file descriptor

  • slave_name : slave device name

  • slave_namesz : slave name size

  • slave_termios : 값이 있으면 slave PTY의 line discipline를 해당 구조체로 초기화

  • slave_winsize : 값이 있으면 slave 의 window 사이즈를 해당 구조체로 초기화

  • pty_fork 루틴 (책에 코드 있음. 아래 설명으로 대체)

    • pty_master를 오픈한 후 fork를 호출
    • 자식은 ptys_open 호출전 먼저 setsid를 호출해 새로운 세션을 만들어야 함
      • setsid 호출 시점에서 자식은 프로세스 그룹 리더가 아니므로 다음 세 단계가 수행
        • 자식을 세션리더로 하는 새 세션이 생성
        • 자식에 대한 새로운 프로세스 그룹 생성
        • 자식과 이전 제어 터미널 사이의 모든 관계가 끊어짐
    • 이후 자식은 termios 구조체와 winsize 구조체를 초기화
    • 표준 input, output, error 복제
    • 부모 프로세스는 pty_master와 자식 프로세스의 ID를 돌려줌
pty Program

APUE는 앞의 pty_fork 함수를 이용하여 pty프로그램을 만듦. (역시 코드는 책에 있음)

"pty prog arg1 arg2" 형태로 실행하여, pseudo terminal에연결된 자신만의 세션에서 실행되도록

pty 프로그램의 코드를 보면,

  • pty_fork 호출 후 자식 프로세스는 필요에 따라 pty-slave echo를 끄고 (set_noecho()) execvp 호출
  • 이후 나머지 모든 argument 들을 자식 프로세스에 넘겨줌
  • 부모 프로세스는 필요 시 exit 호출 시 터미널 상태를 복원하는 종료 처리부 등록
  • 부모는 표준 입력에서 받은 모든 내용을 pty-master에 복사하고 pty-master에서 온 모든 내용을 표준출력으로 복사
  • 하나의 프로세스 내부에서 select와 poll을 사용하거나 여러 Thread를 사용하는것도 가능
Using the pty Program

pty 프로그램을 사용하는 예제

대화식 프로그램 작업제어

pty cat

위 명령을 실행하면, 아래와 같은 구조로 실행된다고 함
19.13

pty_fork 하고 exec 해서 cat 만들고, pty부모가 또 fork 해서 pty 자식이 생김.
이때, cat은 고아 이므로 ctrl+z 가 전달이 되지 않음. 아마 pty 가 먹어버리는 듯??
이런 작업제어가 잘 되게 하려면, pty에 로직이 따로 필요함.

script

pty "${SHELL:-/bin/sh}" | tee typescript

위 명령을 실행하면 아래와 같은 구조로 실행 됨.

19.14

이해가 안감... ㅠㅠ 해보니까 되긴 되는데

그 외

이 외에도 몇가지 예제가 있는데, 다 똑같음.

Advanced Features
  • Packet Mode
    • PTY master가 PTY slave의 상태 변화를 알게함
    • 특정 이벤트가 발생할때 PTY master위에 프로세스가 PTY master를 읽게 만드는데 사용
  • Remote Mode
    • PTY master는 PTY slave에 remote mode를 설정할 수 있음
    • remote mode가 되면 line discipline이 어떤 작업도 수행하지 않음
    • 자체적으로 line editing을 사용하는 프로그램에 사용
  • Window Size Changes
    • PTY master 위에 있는 프로세스는 윈도우 사이즈 변경을 PTY slave의 foreground 프로세스 그룹에게 알려줄 수 있음
  • Signal Generation
    • PTY master 위에 있는 프로세스는 PTY slave의 프로세스 그룹에게 signal를 전송할 수 있음