목차
  1. Introduction
  2. Terminal logins
  3. Network logins
  4. Process groups
  5. Sessions
  6. Controlling terminal
  7. Funcs ( tcgetpgrp, tcsetpgrp, tcgetsid)
  8. Job control
  9. Shell execution of programs
  10. Orphaned process groups
  11. FreeBSD Implementation
  12. Summary


Introduction

이번챕터에서는 process group에 대해 좀 더 자세히 알아보고, session의 컨셉에 대해 이야기할 것이다.

그리고 login shell과 login shell로부터 실행된 모든 process간의 관계에 대해서도 알아볼 것이다.

이 둘의 관계를 설명하려면 '시그널'을 빼놓고 할 수가 없다. 그래서 시그널에 대해 이야기하기 위해 필요한 여러가지 컨셉들에 대해서도 알아야 함.
만약 UNIX 시스템의 시그널 매커니즘에 익숙하지 않다면, 미리 챕터10을 훑어봐야할지도 모름.



Terminal logins

우리가 UNIX 시스템에 로그인할때 실행되는 프로그램에 대해 알아보자.

예전 UNIX (version 7 같은) 에는 host에 hard-wired connected된 멍청한 터미널을 썼었음.
터미널은 두가지 종류가 있었다. local ( direct connected ) 와 remote ( connected through a modem) 이다.
둘중 어느쪽이든 커널안에 있는 terminal device driver를 통해 로그인함.
호스트는 고정된 갯수의 terminal device를 갖고 있었고, 따라서 동시에 접속가능한 로그인의 개수 제한이 있었다.

bitmapped graphical terminal이 가능하게 되면서, 유저에게 host에 접근하는 새로운 방법을 제공하기 위해 windowing system이 개발됨.
character-based terminals를 에뮬레이트하는 "terminal window"라는 것을 만들기 위해 어플리케이션들이 개발되었고, 유저들을 친숙한 방법으로 host에 접근할수 있도록 해줌. (예를들어 shell commnad line을 통해서라던지..)

오늘날, 어떤 플랫폼들은 로그인 후에 windowing system을 시작하도록 하는 것들도 있지만, 자동으로 windowing system이 시작되버리는 것들도 있다. 후자의 경우에도 아마 login은 해야할 것이다. (windowing system을 어떻게 configure할지를 알야하기 때문에).
이제부터 얘기할 프로시저(절차)는 터미널을 써서 UNIX 시스템에 로그인하는 것임. (chracter-based 터미널이든 character-based를 에뮬레이트하는 graphical terminal이든, window system을 실행시키는 graphical terminal이든 뭐든  타입에 관계없이 다 비슷함)

BSD Terminal Logins
bsd terminal login 과정은 35년동안 변한게 없음. system admin은 파일을 만든다 (보통 /etc/ttys)
파일은 터미널디바이스 마다 한줄씩 갖고있음. 각 라인은 device name 과 'getty' 프로그램에 넘겨지는 파라미터들을 갖고 있음. (예를들어 baud rate초당 심볼수 라던지. )
시스템이 부팅(bootstrap)될 때, 커널은 process ID 1를 만들고(init process), 'init'이 system을 multiuser mode로 만든다. 

init 프로세스는 etc/ttys를 읽고 login 되도록 허용된 터미널들에 대해 fork를 하고나서 getty 프로그램의 exec을 한다.


위 그림에 있는 모든 프로세스는 real userID 0, effective user ID 0를 갖는다. (superuser privilege를 가짐)
init 프로세스는 getty 프로그램을 empty environment로 exec함.


getty는 terminal device를 read + write로 open 한다.
device가 모뎀이면, 'open'은 device driver내에서 delay된다. (model이  다이얼 걸고 걸릴때까지)
device가 open 되면, fd 0, 1, 2가 device에 set됨.
그러고나면 "login : "을 출력하고 유저가 username을 치는걸 기다림.
username을 치고나면 getty의 할일은 끝.
username을 입력받으면, getty는 다음과 같이 login 프로그램을 invoke시킴.
     
execle("/bin/login", "login", "-p", username, (char *)0, envp);

gettytab file을 수정해서 다른 프로그램을 invoke하게 할 수 있지만 default는 login 프로그램이다.

'init'은 'getty'를 empty environment로 invoke시키지만,
getty는 login 프로그램을 위한 environment를 설정한다. (environment string들은 gettytab file에 적혀있음) 


지금까지의 과정을 그림으로 나타내면 위와 같음.
모든 프로세스는 슈퍼유저권한을 가지며, 아래 3개의 process는 PID가 같다. (exec으로는 pid가 안바뀐다) 그리고 3개프로세스의 parent의 PID는 1이다.

login 프로그램
  1. username을 받으면, getpwnam을 콜해서 password file을 가져옴.
  2. getpass call해서 "Password: " 을 출력하고 패스워드 입력받음.
  3. crypt call. -> 입력받은 패스워드를 encrypt해서 shadow password file의 encrypted된 password (pw_passwd)와 비교.
  4. 잘못된 패스워드로 로그인이 실패하면, exit with 1. 그러면 parent 'init' 프로세스가 받아서 새로 fork를 해서 getty를 exec함.

이방식은 전통적인 UNIX 시스템에서 사용되는 방법임. 근데 multiple authentication procedure를 지원하는 시스템(FreeBSD / Linux / Mac OS X / Solaris)들은 좀더 flexible한 방식을 제공한다. PAM( Plugged authentication Modules )이라 불리는 방식인데. PAM은 admin이 PAM lib을 쓰기 위한 access service들에 사용되는 authentication method를 configure하는걸 허용함. 
 예를들어, user가 task를 실행하기 위한 appropriate permission을 갖고 있다는 걸 증명해야하는 어플리케이션이 있을 때, 어플리케이션에 authentication mechanism을 하드코딩할수도있지만, PAM library를 사용할수도 있다.
PAM은 유저를 인증하는 방법을 task에 따라 다르게 구성할수 있도록한다.
(프로그램의 재컴파일 없이 인증방식을 바꿀수 있다는 점이 큰 장점이라고한다.)

우리가 login을 성공적으로하게 되면 login 프로그램은 다음의 동작을 한다.
  1. home 디렉토리 변경 ( chdir )
  2. terminal device의 ownership 변경 ( chown ) 을 해서 user가 터미널디바이스를 소유하도록 함.
  3. terminal device에 대한 access permission 변경. -> 유저가 read / write 가능하도록.
  4. group ID 설정 ( setgid , initgroups) 
  5. 환경변수 초기화 ( HOME / SHELL / USER / LOGNAME / PATH)
  6. user ID 변경 ( setuid )
  7. invoke login shell.
execl("/bin/sh" , "-sh", (char*)0);
이외에도 더 많은 동작을 함.

login shell의 parent는 init이다. 따라서 login shell 이 terminate되면 init이  SIGCHLD 시그널을 받게되고, terminal을 위한 일련의 과정을 다시 한번 하게 될 것이다.
login shell의 fd 0, 1, 2가 terminal device에 set됨.


여기까지 되면 위의 그림와 같은 상태가 됨.

그러고나면 login shell은 startup file을 읽는다.
(.profile이나 .bash_profile, .bash_login, .cshrc .login 등등 쉘에 따라 다름)
startup file은 환경변수를 바꾸거나 추가한다.

이게 끝나고나면 이제 우리는 shell prompt를 받을 수 있고, 커맨드를 입력할 수 있는 상태가 된다.

Mac OS X Terminal logins 
- FreeBSD와 비슷
차이점
- init 대신 launchd 가 수행.
- 시작시 graphical-based login screen이 보임.

Linux Terminal Logins
- FreeBSD와 아주 비슷.
차이점
- terminal configuration이 정해지는 방식이 다름.
  1. System V's init file format을 따르는 배포판의 경우
    • /etc/inittab에 configuration infomation 들어있음.
  2. Upstart Init을 따르는 배포판의 경우. (ubuntu)
    • /etc/init 에있는 *.conf 파일을 사용.
    • ex. /dev/tty1에 대한 configuration은 /etc/init/tty1.conf에 적혀있음


Solaris Terminal Logins 
- 생략..


Network Logins
이전 login과 가장 큰 차이는 terminal 과 computer가 point-to-point가 아님.
Network login에서 login은 단순한 service다. ( FTP, SMTP 같은..)

로컬에서는 terminal device의 개수를 알고 있기 때문에 각 디바이스마다 getty process를 미리 만들어놓고 있을 수 있지만 네트워크에서는 얼마나 많은 login이 들어올지 알 수 없음.
가능한 로그인들을 미리 만들고 기다리는 대신, network connection request가 도착하길 기다린다.
Terminal login과 Network login을 같은 소프트웨어로 처리하기 위해 'pseudo terminal' 이라는 소프트웨어 드라이버가 사용된다.
pseudo terminal 
  • serial terminal의 행동을 emultate.
  • terminal operation과 network operation 매핑
  • 기타등등

BSD Network Logins

inet 프로세스라는 하나의 프로세스가 대부분의 network connection을 기다린다. (Internet superserver.)

시스템이 부팅될때, 'init'은 shell을 invoke 시켜 /etc/rc의 shell script를 실행시키도록한다.
이 shell script에 의해 실행되는 데몬들 중 하나가 'inetd'.
shell script가 terminate 되고나면 inetd의 parent는 init이 됨.
inetd는 TCP/IP connection request를 기다린다. 
리퀘스트가 도착하면 inetd는 fork되고, 적절한 프로그램을 exec함.

예를들어 TELNET 서버에 대한 TCP/IP connection request가 도착했다고 치자.
(TELNET은 TCP 프로토콜을 쓰는 remote login application이다) 
유저는 다른 host에 있거나 같은 host에서 telnet client를 통해 로그인을 시도했을 것이다.

telnet hostname

client는 hostname과의 TCP connection을 open.
host는 TELNET 서버라는 프로그램을 실행.
클라이언트와 서버는 TELNET 프로토콜을 써서 TCP connection을 통해 데이터를 주고받는다.

이렇게 그림처럼 되고나면 , telnetd는 pseudo terminal device를 열고 fork를 통해 2 process로 쪼개진다.
parent는 network connection을 통한 communication을 핸들링
child는 fd 0,1,2를 pseudo terminal에 열고, login 프로그램을 exec.
이후는 terminal login에서 말했던것과 같음.
child -> login -> login shell이 됨.



init - login shell - pseudo terminal device driver - telnetd ~~~ telnet client - user.

Mac OS X Network Logins
  • telnetd 대신 launchd가 telnet을 실행시킨다는것 빼고 똑같음.

Linux Network Logins
  • BSD와 같음. 일부 배포판에서 inetd 대신 xinetd를 쓰기도함. 

Solaris Network Logins
  • 생략


Process groups

프로세스 그룹이란 하나이상의 프로세스의 집합이다. 프로세스의 집합은 같은 jobs에 연관이 있는 애들끼리 묶임.
그리고 프로세스 그룹은 같은 터미널에서 signal을 받을 수 있다.

getpgrp을 통해 process group id 얻을 수 있음.
#include <unistd.h>
pid_t getpgrp(void);

pid_t getpgid(pid_t pid); //pid의 process group ID 리턴.

// getpgid(0) == getpgrp()

Process Group은 Leader를 가질 수 있다.
leader가 terminate 되도 group은 존재.
group내 프로세스가 하나라도 있으면 존재함.

리더는 process ID == process group ID

setpgid를 통해 새롭게 group을 만들거나 이미 있는 group에 join할수 있다.

#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);             //return : 0 if OK, -1 on Error
pid를 가진 프로세스의 process group id를 pgid로 설정함.

두 argument가 같으면 pid가 group의 리더가 됨.
pid가 0이면 caller가 사용됨.
pgid가 0이면 pid가 pgid로 사용됨.

자기 자신 혹은 자식들의 process group만 변경가능.
child가 exec을 하고나면 process group 변경 불가능.

대부분의 job-control shell에서 fork후에 불림.
parent가 child의 process group ID 세팅함.
또 child가 자기 자신의 process group ID를 세팅함.
중복인것 같지만 이렇게 두번 불림으로 인해 child가 자신의 process group id를 갖는다는 걸 개런티 가능.
둘중에 하나만 부르면 race condition에 따라서 어떤 프로세스 하나가 execute를 먼저해버리면 개런티 못함.
  

Sessions

세션 내의 배치를 보여줌.

process group 내의 process들은 보통 shell pipeline 에 있는 애들임.

A pipeline of three programs run on a text terminal

#include <unistd.h>
pid_t setsid(void);     //return : process group ID if OK, -1 on Error

setsid를 통해 새로운 세션 만들 수 있음.

setsid를 call했을때, calling process가 process group leader 가 아닌 경우,
     1. process가 새로운 세션의 리더가 됨.
     2. process가 새로운 process group의 리더가 됨. (pgid는 pid가 됨.)
     3. process는 controlling terminal을 갖고있지않게 됨. (controlling terminal은 다음섹션에서 설명)
     3-1. 만약 setsid를 부르기 전 controlling terminal을 갖고 있었다면 관계 파기됨.

calling process가 process group leader인 경우 return error.
아마 process group id 가 이전 세션에 있는데 새로운 세션에 같은 ID로 생기기 때문에 에러가 되는듯.

그래서 보통 이러한 경우를 없애기 위해서, fork를 한후 parent를 terminate 시키고 child만 continue하는 방식을 사용함.
child는 process group leader 가 아닌것을 개런티할 수 있기 때문.

UNIX Specification에는 "session leader"라는 말만 있을 뿐 "Session ID"라는 것은 없다.
session leader는 unique process ID를 가진 single process다. 따라서 session ID에 대해 말한다면 session leader의 PID가 되겠다.


#include <unistd.h>
pid_t getsid(pid_t pid);          //return : session leader's process group ID if OK, -1 on Error
getsid(0)를 콜하면 calling process의 session leader의 pgid를 리턴함.

 
Controlling terminal
세션과 프로세스 그룹의 특징.
  • 세션 하나에 하나의 Controlling Terminal이 연결될 수 있다.
  • Controlling terminal과 연결된 프로세스(세션리더)를 controlling process 라고 한다.
  • 세션에 속한 프로세스 그룹은 2가지로 나눌수 있다.
    • Foreground Process Group / Background Process Group.
  • 세션은 하나의 Foreground Process Group만 가질 수 있다. 나머지는 전부 background.
  • terminal의 interrupt key ( Ctrl + C )를 누르면, Foreground Processs Group의 모든 프로세스에interrupt signal이 날아감.
  • terminal의 quit key ( Ctrl + \ ) 를 누르면, Foreground Process Group의 모든 프로세스에 quit signal이 날아감.
    • 이러한 이유로 daemon은 무분별한 signal을 받으면 안되기 때문에 controlling terminal이 없다고 위키에 적혀 있는데..?
      • 데몬이 속한 프로세스그룹이 속한 세션이 컨트롤링 터미널을 갖지 않는다는 말인지?
      • foreground만 안되면 되는거 아닌가? foreground - background process 그룹이 바뀌는 경우는??
  • 네트워크가 끊어졌음을 terminal device가 인식하게 되면 controlling process(세션리더)에 hang-up signal을 날린다.
  • 보통의 경우 유저가 Contorlling terminal을 신경 쓸 일은 없다. 로그인시 자동으로 controlling terminal이 연결된다.

POSIX.1
controlling terminal 배정하는 매커니즘의 선택을 개별 구현에 맡김.
UNIX System V
세션리더가 세션에 연관되어 있지 않은 terminal device를 열때 그 세션에 control terminal을 배정한다.
(단, open 함수에 O_NOCTTY 플래그를 주지 않았을때에만)
BSD
세션리더가 ioctl(TIOCSCTTY)를 호출 할 때 배정한다. open시 사용하는 O_NOCTTY 플래그는 BSD에서 호환성모드일때만 사용되고, 일반적으로 사용되지 않음.
Mac OSX
bsd처럼 안하고 System V처럼 배정함. 

프로그램에서 /dev/tty file을 open함으로써 controlling terminal와 통신하고 있음을 보장가능.



Funcs ( tcgetpgrp, tcsetpgrp, tcgetsid)


#include <unistd.h>
pid_t tcgetpgrp(int fd);     //Returns  : foreground process group ID if OK, -1 on error.

int tcsetpgrp(int fd, pid_t pgrpid);     //returns : 0 if OK, -1 on error.

tcgetpgrp를 통해 fd에 대해 열려 있는 터미널과 연관있는 foreground process group의 ID를 알아낼수 있다.

tcsetpgrp를 통해 foreground process group을 설정할 수 있다.
  • 제어터미널을 가진 프로세스만 설정 가능.
  • pgrpid는 반드시 같은 세션에 있는 프로세스 그룹의 ID이어야 한다.
  • fd는 반드시 그 세션의 제어 터미널에 대한 file description이어야 한다.

대부분의 어플리케이션은 이 함수를 직접 호출하지 않음. 이 함수는 주로 job control shell에 의해 호출된다.

#include <termios.h>
pid_t tcgetsid(int fd);     //returns : session leader's process group ID 


Job control
1980년경에 BSD에 추가된 기능. 하나의 터미널에서 여러개의 작업(프로세스그룹)을 시작할수 있으며, 특정 작업이 터미널에 접근할 수 있게 만들거마 백그라운드에서 실행되게 만들 수 있다.
job control을 위해서는 세가지 조건이 필요.
  1. job control을 지원하는 쉘이 있어야함.
  2. job control을 지원하는 terminal driver.
  3. 커널이 job-control signal을 지원해야 함.

job이란? -> process 묶음.
ex.
$ vi main.c
쉘에 이렇게 커맨드를 내리면 하나의 프로세스로 구성된 job을 foreground에서 시작함.

$ pr *.c | lpr &
$ make all &
이렇게하면 두개의 job을 background에서 시작.
그리고 background에서 invoke 되는 모든 process는 background로 동작.

유저가 background job을 시작하면 쉘은 job identifier를 부여하고, 하나 이상의 process ID를 출력한다.

$ make all > Make.out &
[1] 1475
pr *.c | lpr &
[2] 1490
$                     //그냥 엔터.
[2] + Done          pr *.c | lpr &
[1] + Done          make all > make.out & 
make의 process ID는 1475이고 job id는 1이다.
다음 job은 process ID는 1490이며,  job id 는 2이다.

enter를 한번 눌러주는 것은 쉘이 자신의 프롬프트를 출력할수 있도록 해주기 위해서.
쉘은 background job의 상태변화가 있을때 임의로 출력할수 없음
유저가 새로운 커맨드를 입력할수 있도록 해주기 위해서, 프롬프트를 출력하기 직전에 출력함.
안그러면 우리가 커맨드입력하는 도중에 출력이 일어나서 난장판됨.

터미널 특수 문자가 입력되면 job 과 terminal device사이에 상호작용이 발생.
     - interrupt character ( DELETE or Ctrl + C ) 는 SIGINT 발생시킴.
     - quit character ( Ctrl + \ ) 는 SIGQUIT을 발생시킴.
     - suspend character ( Ctrl + Z) 는 SIGTSTP을 발생시킴.

job control이 terminal driver와 상호작용하는 상황이 또 하나 있음.
foreground process group은 하나뿐이지만 background process group은 여러개일 수 있다.
사용자가 터미널에 입력한 내용은 foreground job만 받을 수 있다. 하지만 background job이 터미널을 읽으려고 해도 오류가 발생하지는 않는다.
terminal driver는 이를 감지해서 SIGTTIN이라는 signal을 보냄.
보통은 이 시그널에 의해 background job이 stop. 


$ cat > temp.foo &  //background로 시작. + stdin 읽으려고 함.
[1] 1681
$
[1] + Stopped (SIGTTIN)     //정지.
$ fg %1          //foreground로 바꿔준다.
cat > temp.foo
hello, world     //이제부터 입력받을 수 있음
^D               //EOF 입력

background job이 만약에 자신의 출력을 터미널에게 보낸다면 어떤일이 일어날까?
-> 시스템에 설정된 옵션에 따라 다르게 동작함. stty 커맨드로 옵션변경가능.

$ cat temp.foo &
[1] 1719
$ hello, world
                    //Enter키 누름
[1] + Done     cat temp.foo & 
$ stty tostop     //background job이 출력하지 못하게함.
$ cat temp.foo &
[1] 1721
$
[1] + Stopped(SIGTTOU)     cat temp.foo &
$ fg %1
cat temp.foo          
hello, world          //출력됨





Shell execution of programs


쉘이 어떻게 프로그램을 실행하는지, 그리고 그것이 프로세스 그룹이나 controlling terminal, session과 어떻게 연관되는지 알아보자.

job control이 되지 않는 쉘에서는 커맨드를 foreground로 시작하든 background로 시작하든,
실행된 프로세스는 shell과 프로세스 아이디만 다르고(PID), 프로세스 그룹아이디(PGID)는 같음.

job control이 지원되는 쉘이라면 process group은 foreground면 sh과 같아야겠지만 background면 sh과 달라야함.
프로세스그룹아이디(PGID) == 세션아이디(SID)(세션리더의프로세스그룹아이디) : 제어터미널과 연결되어있다. )

이런 쉘에서 백그라운드 프로세스가 stdin을 읽으려한다면 어떻게 될까?
job control을 지원하지 않는 쉘은 background prcoess의 standard input을 /dev/null로 재지정함.
그렇기 때문에 백그라운드 프로세스가 stdin을 읽으려하면 바로 EOF만나서 끝남.

만약 stdin을 읽으려는게 아니라 /dev/tty를 열어서 읽으려한다면?

crypt > salaries | lpr &

crypt는 /dev/tty를 열어서 터미널의 설정을 변경하고, input을 읽어들이고, 설정을 다시 되돌리고 끝남.

crypt에 의해서 
Password:
라는 출력이 나오지만
crypt는 현재 background에 있음.
그래서 유저가 입력을 해봐야 커맨드로 입력됨.
그 다음에 입력한 라인이 password로 입력됨.

즉, 결과 예측 불가능.
job control을 지원하는 쉘이라면 SIGTTOUT을 통해 좀더 잘 처리함.

job control을 지원하지 않는 쉘에서 이렇게  세개의 프로세스를 실행하면 쉘은 어떻게 세개의 프로세스를 실행하게 될까?
$ ps -pid,ppid,pgid,sid,comm | cat1 | cat2

PID  PPID  PGID   SID COMMAND
949   947   949   949 sh
1831  949   949   949 sh
1832  1831  949   949 ps
1833  1831  949   949 sh 
이런식으로 출력될수도 있음.
sh가 fork-exec으로 cat 커맨드를 실행하려 했지만, exec이 끝나기 전에 ps가 출력해버린 것.




job control을 지원하는 쉘에서 실행한다면?

$ ps -o pid,ppid,pgid,sid,tpgid, comm | cat1
PID  PPID  PGID   SID TPGID COMMAND
2837 2818  2837  2837  5799 bash
5799 2837  5799  2837  5799 ps
5800 2837  5799  2837  5799 cat1

$ ps -o pid,ppid,pgid,sid,tpgid, comm | cat1 &
PID  PPID  PGID   SID TPGID COMMAND
2837 2818  2837  2837  2837 bash
5801 2837  5801  2837  2837 ps
5802 2837  5801  2837  2837 cat1

// pgid가 5801인데 tpgid는 2837이다.
// 현재 foreground는 bash이고 ps와 ca1은 background 프로세스 그룹임을 알수 있다.
TPGID는 현재 foreground process group id를 나타냄.
job control을 지원하는 않는쉘과 달리 이번에는 ps와 cat이 쉘과 다른 프로세스 그룹으로 만들어짐을 알수 있음.



Orphaned process groups

orphaned process  : parent가 먼저 종료된 프로세스.
프로세스 그룹 전체가 고아가 되는 경우도 있다.

orphaned process group : 모든 구성원의 부모가 그 그룹의 프로세스이거나, 다른 세션에 있는 프로세스인 그룹.
그룹 내 프로세스 중 하나라도 부모가 같은 세션에 있는 다른 프로세스그룹에 있는 경우에는 orphaned prcoess group이 아님.

fork로 자식을 하나 만들고 프로세스를 종료시킬 경우, 자식프로세스는 고아 프로세스그룹이 됨.
fork로 자식을 만들고, 자식이 정지된 상태에서 부모가 종료된다면, 자식프로세스는 다시 시작될수 있을까?

POSIX.1에서는 새로 고아가 된 프로세스그룹에는 반드시 SIGHUP과 SIGCONT가 차례대로 전송되어야 한다고 요구한다.
 
#include    <sys/types.h>
 #include    <errno.h>
 #include    <fcntl.h>
 #include    <signal.h>
 #include    "ourhdr.h"
 static void sig_hup(int);
 static void pr_ids(char *);
 int
 main(void)
 {
    char    c;
    pid_t   pid;
    pr_ids("parent");
    if ( (pid = fork()) < 0)
        err_sys("fork error");
    else if (pid > 0) { /* parent */
        sleep(5);       /* sleep to let child stop itself */
        exit(0);        /* then parent exits */
    } else {            /* child */
        pr_ids("child");
        signal(SIGHUP, sig_hup);    /* establish signal handler */
        kill(getpid(), SIGTSTP);    /* stop ourself */
        pr_ids("child");    /* this prints only if we're continued */
        if (read(0, &c, 1) != 1)
            printf("read error from control terminal, errno = %d\n", errno);
        exit(0);
    }
 }
 static void sig_hup(int signo)
 {
    printf("SIGHUP received, pid = %d\n", getpid());
    return;
 }
 static void pr_ids(char *name)
 {
    printf("%s: pid = %d, ppid = %d, pgrp = %d\n",
            name, getpid(), getppid(), getpgrp());
    fflush(stdout);
 }

$./a.out
parent: pid = 6099, ppid = 2837, pgrp = 6099, tpgrp = 6099
child: pid = 6100, ppid = 6099, pgrp = 6099, tpgrp = 6099
$ SIGHUP received, pid = 6100
child: pid = 6100, ppid = 1, pgrp = 6099, tpgrp = 2837
read error from controlling TTY

SIGHUP 신호를 받고 sig_hup 함수 실행됨. 이후 SIGCONT에 의해 재개됨.
정지되어있어도 signal로 핸들러 정해놓은건 돌아가는듯.

read error 나는 이유.
background 프로세스가 터미널 읽으려고 하면 SIGTTIN 신호가 전송되고, 프로세스가 STOP함.
고아 프로세스 그룹의 프로세스인데 이 신호를 받고 멈출수도 있음. SIGTTIN 받고 멈추면 다시 재개될수없다.
따라서 POSIX.1에서는 이런 상황에서 read가 errno를 EIO로 설정하고 리턴되어야 한다고 명시함.

그리고 재개될때 tpgrp을 보면 자식 프로세스가 background 프로세스 그룹으로 되어있는것을 알수있음.
부모가 쉘이 의해 foreground로 실행되었기 때문이다. (?)


FreeBSD Implementation

위에서 알아본 내용들이 어떻게 실제로 구현되어있는지 알아보자.
FreeBSD를 기준으로 설명함.


이 그림에 있는 structure들을 설명할 것임.

session structure
  • s_count : 해당 Session에 존재하는 Process Group의 갯수. 0이 되면 structure free
  • s_leader : Session Leader의 Proc Structure (Linux의 task_struct) Pointer
  • s_ttyvp : Controlling Terminal의 vnode Structure Pointer
  • s_ttyp : Controlling Terminal의 tty Structure Pointer
  • s_sid : Session ID. Single UNIX Spec은 아님.
  • setsid가 호출 되면 Kernel이 새 Session Structure를 할당.
  • s_count : 1
  • s_leader : Calling Process의 Proc Structure Pointer

s_ttyvp, s_ttyp : Null. 새로운 세션에 아직 제어터미널이 없기 때문. (로그인하면연결된다)

tty structure
  • t_ssession : 이 터미널을 제어터미널로 갖고있는 session 구조체를 가리킴. hang up됬을때 시그널 날리기 위해.
  • t_pgrp : foreground 프로세스 그룹의 pgrp 구조체를 나타냄. SIGINT, SIGTSTP, .. 이런 시그널 보내야하므로.
  • t_termios : 터미널의 모든 특수 문자와 기타 정보 (baud rate 등) 18장에서 다시 설명.
  • t_winsize : 터미널 창의 현재 크기 정보. 크기가 변하게 되면 foreground process그룹에 SIGWINCH 보냄.

pgrp struct
  • pg_id : 프로세스 그룹 id
  • pg_session : 속한 session id
  • pg_members : 이 그룹에 속한 프로세스들의 proc 구조체 리스트.

proc structure
  • p_pid : 프로세스 id
  • p_pptr : parent pointer?. parent의 proc structure를 가리킨다.
  • p_pgrp : 자신이 속한 그룹의 pgrp 구조체를 나타냄.
  • p_pglist : 다음 proc, 이전 proc 포인터.

vnode structure
  • 제어 터미널 (controlling terminal)이 열릴때 할당됨.
  • 한 프로세스에서 /dev/tty에 대한 모든 참조는 이 vnode를 가리킴.



Summary

프로세스들 사이의 관계에 대해 알아봤다.
특히 프로세스 그룹과 세션에 대해 알아봄
job control은 오늘날 대부분의 유닉스 시스템이 지원하는 기능.
job control이 어떻게 동작하는지 알아봤음.












'스터디 > APUE' 카테고리의 다른 글

15. Interprocess Communication  (0) 2017.12.28
5. Standard I/O Library  (0) 2017.12.28
Posted by outshine90
,