15.1 Introduction
chapter8 에서 우리는 Process를 control하는 것들에 대해서 알아봄. (fork, exit, wait, exec, ...등등등)
그리고 어떻게 여러개의 프로세스들이 동작하는지 봤다.
근데 프로세스간에 정보를 교환하는 방법은 오직 fork와 exec을 통하거나 file system을 통해 open file을 넘겨주는 방식만 봤음.
이번 장에서는 프로세스간 커뮤니케이션을 하는 다른 테크닉들에 대해 알아볼 것이다.
예전에는 UNIX System IPC가 뒤죽박죽이라서 UNIX 시스템간에 portable하지 않았는데, 요새는 좀 나아짐.
SUS : Single Unix Specification (UNIX란 이름을 쓰기위해 지켜야 하는 스펙)
dot : 지원한다.
(full) : full-duplex를 이용해 지원함.
UDS : Unix Domain Socket을 이용해 지원함.
XSI : X System Interaface (SVR4에서 물려받은 방식. SysV. 구식)
real-time은 원래는 option이었는데 SUSv4부터 base spec으로 넘어감.
named full-duplex pipe가 마운트된 STREAM-based pipe으로 제공됬음. 근데 SUS에서 없어짐. (no required.)
socket과 STREAMS는 different host 사이에도 됨.
위에것 들은 same host의 process 사이에만 가능.
IPC를 3챕터에 나눠서 설명할것임.
이번 챕터에서는 pipes, FIFOs, message queue, semaphores, shared memory.
다음 챕터 : socket 메커니즘을 이용한 network IPC.
다다음 챕터 : advanced feature of IPC.
15.2 Pipes
Pipes는 UNIX System IPC에서 가장 오래됐고, 모든 UNIX System에서 지원함.
파이프는 두가지 제약사항이 있다.
- 역사적으로 파이프는 하프듀플렉스였다. 어떤 시스템들은 풀듀플렉스를 제공한다. 하지만 최대한 이식성을 살리려면 하프듀플렉스만 있다고 생각해야함.
- parent - child 사이에서 사용가능.
뒤에나올 FIFO는 2번 제약사항이 없다.
이 두가지 제약사항에도 파이프는 가장 널리 사용됨. (쉘에서 커맨드쳐서 실행시 파이프 사용)
#include <unistd.h>
int pipe(int fd[2]);
//Returns : 0 if OK, -1 on error.
2개의 fd가 리턴됨.
fd[0] : for reading.
fd[1] : for write.
fstat을 이용해 fd가 pipe로 열린애인지 알 수 있음. (pipe로 열린 fd면 파일 타입이 FIFO라고 나옴)
S_ISFIFO를 이용해 테스트 해볼수있다.
fstat에 파이프의 read쪽 fd를 넣고 콜해보면, 많은 시스템에서 stat 구조체의 st_size멤버에 파이프에서 읽을 수 있는 바이트 수가 설정되어 나오는데, spec은 아니기 때문에 사용하지 않는것이 좋음.
two ways to view a harf-duplex pipe
싱글 프로세스에서 파이프는 쓸모가 없음.
보통 프로세스는 pipe를 콜하고, fork를 해서 IPC 채널을 만든다.
fork를 콜한 후 모습.
그다음엔 어떻게 할까?
우리가 어떤 data flow를 원하느냐에 달려있다.
from parent to child라면.
parent는 read end(fd[0])를 닫는다.
child는 write end(fd[1])를 닫는다.
닫고난 후 모습.
파이프의 한쪽 끝이 닫히고 나면 두가지 룰이 적용됨.
- 닫힌 write-end를 읽으려 하면, read return 0. (모든 데이터가 읽혀 나가서 EOF를 만났음을 나타냄) (기술적으로, 우리는 EOF가 더이상 pipe의 writer가 없을때까지 generate되지 않도록 해야한다고 말할 수 있다. 그런일은 pipe descriptor가 복사되어 여러 프로세스가 그 파이프를 write용으로 열었을 때 가능함. 하지만, 보통 pipe는 single writer, single reader임. 다음에 FIFOs를 배우면 그땐 multiple writer가 나옴.)
- 닫힌 read-end에 쓰려고하면, SIGPIPE 시그널 발생. 시그널을 무시하거나, 잡아서 핸들러한테서 리턴받아야 함(write return -1, errno set to EPIPE).
파이프나 FIFO에 write를 할 때, constant PIPE_BUF 값은 커널의 pipe buffer size를 따른다.
PIPE_BUF 이하의 크기를 write하면 데이터는 인터리브되지 않음.
멀티플 프로세스가 파이프에 write를 하거나 PIPE_BUF보다 큰 값을 write하면, 데이터는 아마 인터리브될것이다.
pathconf나 fpathconf를 이용해서 PIPE_BUF값 설정가능.
Example. 파이프를 만들고 부모가 자식에게 데이터를 보내는 코드.
#include <apue.h>
int main()
{
int n;
int fd[2];
int pid;
char line[MAXLINE];
if (pipe(fd) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0) {
err_sys("fork_error");
} else if (pid > 0) {
/* parent */
close(fd[0]);
write(fd[1], "hello world\n", 12);
} else {
/* child */
close(fd[1]);
n = read(fd[0], line, MAXLINE);
write(STDOUT_FILENO, line, n);
}
exit(0);
}
예제처럼 파이프 fd를 넣고 read /write를 할수도 있다.
더 흥미로운 점은 pipe descriptor를 standard input/ouptut에 duplicate 시켰을때임.
자식이 프로그램을 실행하고, 그 프로그램은 standard input/ouput에 읽고 쓰는데 그게 우리가 만든 pipe가 되도록 할 수 있다.
Example2. 어떤 아웃풋이 만들어지면 한번에 한페이지씩 디스플레이하는 프로그램을 고려해보자.
유닉스시스템유틸리티들이 만들어놓은 페이지네이션을 reinvent 하기보다, 유저가 좋아하는 pager를 인보크시키고 싶다.
모든 데이터를 임시파일로 만드는걸 피하고, 그 파일을 디스플레이하기 위해 'system' 콜하지 않도록 하기 위해서 pipe를 써서 아웃풋을 다이렉트로 pager에게 쏴주자.
이를 위해 우리는 pipe를 만들고, fork하고, 자식의 standard output, input을 pipe로 바꿀것이다.
그리고나서 exec을 호출해서 유저의 페이저 프로그램을 실행할것임.
#include <apue.h>
#include <sys/wait.h>
#define DEF_PAGER "/bin/more"
int main(int argc, char *argv[]) {
int n;
int fd[2];
pid_t pid;
char *pager, *argv0;
char line[MAXLINE];
FILE *fp;
if (argc != 2)
err_quit("usage: a.out <pathname>");
if ((fp = fopen(argv[1], "r") == NULL))
error_sys("cant open %s", argv[1]);
if (pipe(fd) < 0)
error_sys("pipe error");
if ((pid = fork()) < 0)
error_sys("fork error");
else if (pid > 0) {
/* parent */
close(fd[0]); //close read end
/* parent copy argv[1] to pipe */
while (fgets(line, MAXLINE, fp) != NULL) {
n = strlen(line);
if (write(fd[1], line, n) != n)
error_sys("write error to pipe");
}
if (ferror(fp))
err_sys("fgets error");
close(fd[1]); /* close write end of pipe for reader */
if (waitpid(pid, NULL, 0) < 0) //자식프로세스 종료를 기다림
error_sys("waitpid error");
exit(0);
} else {
/* child */
close(fd[1]); //close write end
if (fd[0] != STDIN_FILENO) {
if (dup2(fd[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd[0]);
}
/* get argument for execl() */
if ((pager = getenv("PAGER")) == NULL)
pager = DEF_PAGER;
if ((argv0 = strrchar(pager, '/')) != NULL)
argv0++; //step past rightmost slash
else
argv0 = pager; //no slash in pager
if (execl(pager, argv0, (char*)0) < 0)
error_sys("execl error for %s", pager);
}
exit(0);
}
Example3.
Section8.9에서 TELL_WAIT, TELL_PARENT, TELL_CHILD, WAIT_PARENT, WAIT_CHILD 함수 사용했음.
이걸 10.16에서는 시그널을 이용해 구현했었는데, 이번엔 파이프를 이용해 구현해보겠다.
#include <apue.h>
int main() {
//변수선언및 초기화
// ....
TELL_WAIT(); //set things up for TELL_XXX & WAIT_XXX
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid == 0) {
/* child */
//child routine...
TELL_PARENT(getppid()); //tell parent we're done.
WAIT_PARENT(); //wait for parent.
// the child continue on its way..
exit(0);
}
/* parent */
//parent routine....
TELL_CHILD(pid);
WAIT_CHILD();
//the parent continues on its way..
exit(0);
}
#include <apue.h>
static int pfd1[2], pfd2[2];
void TELL_WAIT(void) {
if (pipe(pfd1) < 0 || pipe(pfd2) < 0)
err_sys("pipe error");
}
void TELL_PARENT(pid_t pid) {
if (write(pfd2[1], "c", 1) != 1)
error_sys("write_error");
}
void WAIT_PARENT() {
char c;
if (read(pfd1[0], &c, 1) != 1)
error_sys("read error");
if (c != 'p')
err_quit("WAIT_PARENT: incorrect data");
}
void TELL_CHILD(pid_t pid) {
if (wirte(pfd1[1], "p", 1) != 1)
err_sys("write error");
}
void WAIT_CHILD() {
char c;
if (read(pfd1[0], &c, 1) != 1)
err_sys("read error");
if (c != 'c')
error_quit("WAIT_CHILD: incorrect data");
}
이런 그림이 됨.
15.3 popen and pclose Functions
다른 프로세스에 파이프를 만드는 것은 그 프로세스의 output을 읽어오거나 input으로 보내기 위해 사용되므로, Standard I/O 라이브러리에서는 popen과 pclose라는 함수를 제공한다.
이 함수는 우리가 그동안 해왔던 지저분한 일을 알아서 해줌. (파이프 만들고 fork하고, 파이프 한쪽 끝 close하고,.. 쉘에게 커맨드를 날리고, 커맨드가 끝나길 기다리고 ....)
#include <stdio.h>
FILE popen(const char *cmdstring, const char *type);
//Return : file pointer if OK, NULL on error.
int pclose(FILE *fp);
//Return : termination status of cmdstring, or -1 on error
popen은 cmdstring을 실행하기 위해 fork 와 exec을 하고 standard I/O file pointer를 리턴한다.
type이 "r"일 경우에는 filepointer가 cmdstring의 standard output에 연결된다.
type이 "w"일 경우에는 cmdstring의 standard input에 연결됨.
pclose는 standard I/O stream을 닫고, 커맨드가 terminate되기를 기다림. 그리고 쉘에서 받은 termination status를 리턴함.
cmdstring은 Bourne shell에 의해
sh -c cmdstring
라고 친것처럼 실행됨. 즉, 쉘이 cmdstring에 있는 쉘 특수문자를 확장한다는 의미.
fp = popen("ls *.c", "r");
fp = popen("cmd 2>&1", "r");
이런거 가능.
Example
아까 만들었던 페이저 예제를 popen을 이용해서 만들어보자.
#include <apue.h>
#include <sys/wait.h>
#define PAGER "${PAGER:-more}" //환경변수.
int main(int argc, char *argv[]) {
char line[MAXLINE];
FILE *fpin, *fpout;
if (argc != 2)
err_quit("usage: a.out <pathname>");
if (fpin = fopen(argv[1], "r")) == NULL)
err_sys()
if (fpout = popen(PAGER, "w")) == NULL)
err_sys()
while (fgets(line, MAXLINE, fpin) != NULL) {
if (fputs(line, fpout) == EOF)
err_sys("fputs error to pipe");
}
if (ferror(fpin))
err_sys("fgets error");
if (pclose(fpout) == -1)
err_sys("pclose error");
exit(0);
}
shell command ${PAGER:-more} 는 PAGER 변수가 설정되어있으면, 그걸 사용하고 설정되어있지않으면 more을 사용함.
Example..
popen과 pclose 구현.
예제코드생략... (길어서)
popen은
pipe 열고 fork하고 close하고 execl 함.
pclose는 fclose로 파일포인터 닫고, waitpid로 차일드 죽을때까지 기다렸다가 stat받아서 리턴.
대부분 생각했던대로 앞 예제에서 해왓던 일들임.
디테일한 부분이 다름. 예를들어 static childpid변수가 있다.
popen시 childpid를 받아서 저장해놓고 pclose할때 써야함.
fileno를 통해 파일포인터를 fd로 바꿈. 그리고 fd에 해당하는 childpid를 지움.
open_max라는 함수도 콜하는데, 이건 최대로 열수있는 파일 수를 나타냄.
즉, 우리는 popen를 사용할때 이 값보다 같거나 커지도록 많이 열어선안됨.
POSOX.1은 이미 popen으로 인해 child에 열려 있는 stream이 있을때, popen으로 또 열게되면 기존에 있던 stream을 모두 닫도록 요구함.
(코드를보면 childpid 변수로 이를 체크하고 close함)
만약 pclose의 caller가 SIGCHLD의 핸들러를 설정했으면 어떤일이 벌어질까?
pclose에서 waitpid를 콜하는데 여기서, EINTR가 리턴될것이다. caller가 이 시그널을 캐치하도록 허가되어있기때문에 interrupted되었다면 다시 waitpid를 호출해주면 된다. (?)
-> waitpid에서 차일드 죽기를 기다리고 있는데, child 죽어서 SIGCHLD 시그널이 날아와서 핸들러 콜하게 되나봄.
-> 그래서 핸들러 끝나고 다시 waitpid호출하도록 while(waitpid(pid, &stat, 0) <0)라고 짯나봄.
-> waitpid에서 시그널받아서 인터럽트걸리면 EINTR를 리턴하나보다.
Example3.
프롬프트를 스탠다드 아웃풋에 적고, 스탠다드인풋으로 들어오는 line을 읽는 어플리케이션을 만든다고 생각해보자.
popen을 이용해서, 우리는 이 어플리케이션과 그것의 input 사이에 어떤 프로그램을 끼울 수 있음.
즉, 유저가 input을 치면 어플리케이션이 먼저 받고, 그걸 필터링해서 프로그램에게 넘겨줌.
transforming input using popen
예를들어, 변환은 pathname 확장자가 될수도 있고, 히스토리 메커니즘이 될수도 있다. (이전에 쳤던 커맨드들을 기억함으로써)
#include <apue.h>
#include <ctype.h>
int main(void) {
int c;
while ((c = getchar()) != EOF) {
if (isupper(c))
c = tolower(c);
if (putchar(c) == EOF)
err_sys("output error");
if (c == '\n')
fflush(stdout);
}
exit(0);
}
이 코드를 myuclc로 컴파일한다.
#include "apue.h"
#include <sys/wait.h>
int main() {
char line[MAXLINE];
FILE *fpin;
if ((fpin = popen("myuclc", "r")) == NULL)
err_sys("popen error");
for (;;) {
fputs("prompt> ", stdout);
fflush(stdout);
if (fgets(line, MAXLINE, fpin) == NULL) //read from pipe
break;
if (fputs(line, stdout) == EOF)
err_sys("fputs error to pipe");
}
if (pclose(fpin) == -1)
err_sys("pclose error");
putchar('\n');
exit(0);
}
15.4 Coprocesses
Unix system에서 필터는 stdin에서 읽고, stdout에 출력하는 프로그램이다. 한 프로그램이 만든 output을 필터가 읽고 필터의 아웃풋을 같은 프로그램이 읽을때, 필터는 coprocess가 된다.
콘쉘은 coprocesses를 제공.
본쉘, 본어게인쉘, C쉘은 프로세스들을 coprocesses로 만드는 방법을 제공해주지 않음.
coprocess는 보통 쉘의 백그라운드에서 돌아감.
coprocess를 만드는 쉘구문은 좀 contorted하다, 하지만 coprocess는 C프로그램에서 유용함.
popen을 하면 one-way pipe가 연결됨.
coprocess를 위해서는 two-way가 필요함.
#include "apue.h"
int main() {
int n, int 1, int2;
char line[MAXLINE];
while ((n = read(STDIN_FILENO, line, MAXLINE)) > 0 ){
line[n] = 0;
if ((sscanf(line, "%d%d", &int1, &int2) == 2) {
sprintf("line, "%d\n", int1 +int2);
n = strlen(line);
if (write(STDOUT_FILENO, line, n) != n)
err_sys("write error");
} else {
if (write(STDOUT_FILENO, "invalid args\n", 13) != 13)
err_sys("write error");
}
}
exit(0);
}
이 코드를 add2라는 파일로 컴파일함.
다음예제에서 add2를 인보크시켜서 coprocess하는 예제를 써볼거임.
#include "apue.h"
static void sig_pipe(int); //our signal handler
int main() {
int n, fd1[2], fd2[2];
pid_t pid;
char line[MAXLINE];
if (signal(SIGPIPE, sig_pipe) == SIG_ERR)
err_sys("signal error");
if (pipe(fd1) < 0 || pipe(fd2) < 0)
err_sys("pipe error");
if ((pid = fork()) < 0)
err_sys("fork error");
else if (pid > 0) {
/* parent */
close(fd1[0]);
close(fd2[1]);
while (fgets(line, MAXLINE, stdin) != NULL) {
n = strlen(line);
if (write(fd1[1], line, n) != n)
err_sys("write error to pipe.");
if ((n = read(fd2[0], line, MAXLINE)) < 0)
err_sys("read error from pipe");
if (n == 0) {
error_msg("child close pipe");
break;
}
line[n] = 0;
if (fputs[line, stdout) == EOF)
err_sys("fputs error");
}
if (ferror(stdin))
err_sys("fgets error on stdin");
exit(0);
} else {
/* child */
close(fd1[1]);
close(fd2[0]);
if (fd1[0] != STDIN_FILENO) {
if (dup2(fd1[0], STDIN_FILENO) != STDIN_FILENO)
err_sys("dup2 error to stdin");
close(fd1[0]);
}
if (fd2[1] != STDOUT_FILENO) {
if (dup2(fd2[1], STDOUT_FILENO) != STDOUT_FILENO)
err_sys("dup2 error to stdout");
close (fd2[1]);
}
if (execl("./add2", "add2", (char*)0) < 0)
err_sys("execl error");
}
exit(0);
}
static void sig_pipe(int signo) {
printf("SIGPIPE caught\n");
exit(1);
}
add2 프로세스를 invoke 시켜서 보조업무를 맡기는 방식?
15.5 FIFOs
FIFOs는 named pipe라고도 한다.
unnamed pipe는 related process(parent - child) 사이에서만 사용 가능.
FIFOs는 unrelated processes간에도 통신가능.
#include <sys/stat.h>
int mkfifo(const char *path, mode_t mode);
int mkfifoat(int fd, const char *path, mode_t mode);
//return : 0 if OK, -1 on error
mkfifoat에서
path가 절대경로일때 : mkfifo와 같음. (fd무시)
상대경로일때 : fd의 디렉토리로부터 상대경로 계산.
- fd가 AT_FDCWD이면 현재 디렉토리부터 상대경로
mkfifo / mkfifoat을 통해 FIFO를 한번 만들고나면, 우리는 open 함수를 통해 그걸 열 수 있음.
그러고나면 normal file I/O function들을 사용가능.(close, read, write, unlink... )
FIFO를 open으로 열때 nonblocking flag(O_NONBLOCK)에 따라 다음과 같은 일이 생김.
- normal case(without nonblock), 읽기전용으로 open하면 다른 프로세스가 fifo를 write로 열때까지 블락됨.
비슷하게 쓰기전용으로 FIFO를 열면, 다른 프로세스가 피포를 읽으려고 열기전까지 블락됨.
- O_NONBLOCK 으로 연 경우 :
open for read-only는 즉시 return 됨.
open for write-only는 -1을 리턴하고, errno를 ENXIO로 설정함 (FIFO를 read로 연 프로세스가 없을 때)
파이프와 마찬가지로, reader가 없는 FIFO에 write하는 경우 SIGPIPE 발생. 마지막 FIFO의 writer가 FIFO를 close한 경우, FIFO의 reader에게 EOF가 발생함.
FIFO에 multiple writer가 있는 경우는 일반적임. 이는 우리가 atomic writes에 대해 주의해야한다는 뜻.(interleave되지 않도록) 파이프에서와 마찬가지로 PIPE_BUF값에 맞춰서 data를 보냄으로써 우리는 atomic하게 write할수있음.
FIFOs의 두가지 사용법.
- 임시파일을 만들지 않고 하나의 쉘 파이프라인으로부터 다른 데에 쉘커맨드를 넘길때 사용.
- client-server 어플리케이션에서 클라이언트와 서버사이의 데이터를 넘겨주는 rendezvous points로 사용됨.
Example 1 - FIFOs를 써서 아웃풋 스트림을 복제하기.
Example 2 - FIFO를 사용한 클라이언트 - 서버 통신.
15.6 XSI IPC
XSI ICP에는 세가지 타입의 IPC가 있음. - message queues, semaphores, shared memory.
세가지는 비슷한점이 많은데, 이번 섹션에서는 비슷한 특징들에 대해 말할것임.
다음섹션에는 각 IPC의 specific function들에 대해 알아볼것임.
15.6.1 Identifiers and Keys
15.6.2 Permission Structure
15.6.3 Configuration Limits
15.6.4 Advantages and Disadvantages
Identifiers and Keys.
세가지 IPC(메세지큐, 세마포어, 쉐어드메모리) 스트럭쳐들은 non-negative integer인 identifier에 의해 참조됨.
스트럭쳐를 만들었다 지웠다를 반복해보면, identifier는 maximum positive value까지 커지다가 0으로 되돌아감.
identifier는 IPC 오브젝트의 내부적인 이름이다. 프로세스는 같은 IPC object와 통신하기 위해서 external naming scheme이 필요함. 이런 목적에서 IPC 오브젝트는 key라는 걸 사용.
IPC 스트럭쳐가 만들어지면 반드시 key도 정해짐. key_t라는 시스템 프리미티브 타입을 갖는데, <sys/types.h>에 보통 long int로 정의되어있음. Key는 커널에 의해 identifier로 변환됨.
클라이언트와 서버가 같은 IPC 스트럭쳐로 만날 수 있는 방법.
- 서버는 IPC_PRIVATE라는 키로 IPC 스트럭쳐를 만들고 리턴된 identifier를 파일 같은곳에 저장. IPC_PRIVATE는 서버가 새로운 IPC 스트럭쳐를 만들 수 있도록 개런티함. 이 방법의 단점은 서버가 identifier를 파일에 쓰도록 system operation을 써야한다는 점, 그리고 이후 클라이언트가 이를 찾아야 한다는 점.
- IPC_PRIVATE는 parent-child 관계에서도 사용됨. 부모가 IPC_PRIVATE로 IPC 스트럭쳐를 만들고, 받은 id는 child가 사용함. child는 exec시 아규먼트로 id를 받을 수 있다.
- 서버와 클라이언트는 common header에 key를 미리 define해놓고 사용. 서버는 명시된 키로 IPC 스트럭쳐를 만들 수 있음. 문제는 이 키가 이미 IPC스트럭쳐에서 사용중일때다. 이런 경우 get 함수는 error를 리턴함. 서버는 반드시 이 에러를 핸들링해서 존재하는 IPC 스트럭쳐를 지우고, 다시 시도해야함.
- 클라이언트와 서버가 미리 정의된 pathname과 project ID(character value 0 ~255)를 알고 있는 경우. ftok함수를 이용해 이 두가지 밸류를 key로 변환함. 이후는 2번방법과 같다.
#include <sys/ipc.h>
key_t ftok(const char *path, int id);
Returns : key if OK, -1 on error
msgget , semget, shmget는 모두 비슷한 아규먼트를 가지는데, key와 flag를 받는다.
key에 IPC_PRIVATE를 주면 새로운 IPC 스트럭쳐를 만들수 있다.
key가 IPC_PRIVATE이 아니더라도 현재 존재하는 IPC 스트럭쳐와 관계가없는 새로운 키값과 IPC_CREATE flag를 통해 새로운 IPC 스트럭쳐 생성가능.
IPC 스트럭쳐를 만들때, 기존의 스트럭쳐를 레퍼런싱하지 않고 새롭게 만들것을 보장하고 싶을 때에는 IPC_CREAT와 IPC_EXCL flag를 같이 사용하면 된다. 기존에 스트럭쳐가 존재하면 EEXIST 에러를 리턴한다.
(파일에서 O_CREAT와 O_EXCL와 비슷)
Permission Structure
ipc_perm 스트럭쳐는 해당 IPC 오브젝트의 퍼미션과 오너를 나타냄.
struct ipc_perm {
uid_t uid; // owner's effective user id
gid_t gid; // owner's effective group id
uid_t cuid; // creator's effective user id
gid_t cgid; // creator's effective group id
mode_t mode; //access mode
}
/sys/ipc.h에 보면완벽한 정의를 볼 수있다. (위에꺼에서 몇개 더잇을걸)
IPC 스트럭쳐를 생성할때 값들이 초기화되며, 후에 msgctl, semctl, shmctl 등을 사용해서 값들을 변경할 수 있다.
값을 변경하기 위해서는 calling process가 반드시 IPC 스트럭쳐를 생성한 프로세스와 같거나 superuser여야 함.
(chmod, chown 이런 함수랑 비슷)
Configuration Limits
XSI IPC 의 세가지 폼은모두 빌트인 리미트를 가지고 잇음. 대부분의 리밋들은 커널을 configuration을 바꾸면 바꿀수 있음. 자세한건 이후에..
Advantages and Disadvantages
단점
- IPC 스트럭쳐가 시스템와이드한데, 레퍼런스카운트를 가지고 있지 않음. 예를들어 우리가 메세지큐를 만들고, 큐에 메세지를 집어넣은다음에 터미네이트되면, 메세지큐와 메세지들은 지워지지않고 남아있게됨.. 다른 어떤 프로세스가 읽어가거나 msgrcv, msgctl, ipcrm 등으로 지워주거나 시스템이 리붓될때까지 남아있음.
pipe와 비교하면 파이프는 마지막 레퍼런스를 가진 프로세스가 터미네이트되면 완벽히 삭제됨.
FIFO와 비교하면, name은 파일시스템에 남아있지만 데이터는 파이프와 마찬가지로 삭제됨.
- 파일시스템에서 네임으로 접근불가.
기존 커맨드 (ls, rm, chmod) 등 사용 불가. 이거때매 새롭게 추가된 system call들을 사용해야함.
fd를 사용할 수 없기때문에 멀티플렉스I/O 함수(select, poll) 등도 사용할 수 없다. 이는 여러개의 IPC 스트럭쳐를 동시에 사용하기 어렵게만듬. 두개의 메세지큐를 기다리려면 busy-wait loop을 사용하는 수밖에없다.
장점
어떤 책에서는 앞에서 말했던 네임의 문제가 장점이라고 함. 메세지를 메세지큐에 보낼때 open, write, close등 여러함수를 쓸 필요없이 msgsnd 하나로 해결되기때문.
-> 이말은 틀렸음.
또 다른 장점으로는 connectionless, reliable, flow control, recored oriented, 등등..이 있다더라.
- connectionless : 먼저 open 같은 함수를 부르지 않고 메세지를 바로 보낼수 있느냐.
-> 이말도 틀림. queue에 메세지를 보내려면 먼저 identifier를 얻어야하기때문.
- reliable : 모든 XSI IPC가 싱글호스트로 제한되기때문에 reliable한건 맞음.
- flow control : buffer가 모자라거나 리시버가 메세지를 더이상 받지않을때, 센더가 sleep으로 들어가고, 제어 조건이 풀리면 자동으로 sender가 깨어나는것.
15.7 Message Queue
커널에 저장되어있는 메세지들의 링크드 리스트이다.
queue와 queue ID를 통해 메세지를 호출 가능.
msgget : 존재하는 메세지큐를 얻거나 새로운큐를 생성함.
msgsnd : 새로운 메세지들을 큐에 넣음.
msgrcv : 메세지 가져옴. (꼭 fifo 방식으로 가져올 필요는없음)
모든 메세지는
positive long int type,
non-negative length,
actual data bytes를 가진다.
메세지 큐는 msqid_ds 스트럭쳐를가짐.
struct msqid_ds {
struct ipc_perm msg_perm;
msgqnum_t msg_qnum; //큐 안에 있는 메세지 수
msglen_t msg_qbytes; //큐의 max bytes.
pid_t msg_lspid; //pid of last msgsnd()
pid_t msg_lrpid; // pid of last msgrcv()
time_t msg_stime; //last send time
time_t msg_rtime; //last rcv time
time_t msg_ctime; //last change time
}
스트럭쳐는 큐의 현재 상태를 나타냄.
그리고 각 os마다(FreeBSD, Linux, MacOS, Solaris) limit 값들이 책에 나와있음.
#include <sys/msg.h>
int msgget(key_t key, int flag);
//Returns : message queue ID if OK, -1 on error.
앞에서 우린 key를 identifier로 컨버팅하는 룰과 큐가 새로 생길지, 기존의 큐를 레퍼런싱할지를 배웠다. (?)
생성한다면 msqid_ds스트럭쳐 초기화함.
함수가 성공하면 non-negative queue ID를 리턴.
이 값은 다른 세가지 메세지큐 함수에 사용됨.
#include <sys/msg.h>
int msgctl(int msgid, int cmd, struct msgid_ds *buf);
//Return : 0 if OK, -1 on error
이 함수는 다양한 동작을 함. ioctl의 XSI IPC 버전이라고 생각하면 됨.
cmd : msqid의 큐에 동작할 커맨드
IPC_STAT : 현재 큐의 msqid_ds를 버퍼에 저장.
IPC_SET : 버퍼를 현재 큐의 msqid_ds에 세팅.
IPC_RMID : 메세지 큐와 데이터를 즉시 삭제. 해당 메세지큐를 사용하고 있던 다른 프로세스에서 큐에 접근하려고 하면 EIDRM 에러를받게됨. (권한을 가진 프로세스만 이 커맨드 사용가능. msg_perm.cuid, msg_perm.uid 참고)
뒤에 semaphore 와 shared memory에서도 이 커맨드들 사용.
#include <sys/msg.h>
int msgsnd(int msqid, const void *ptr, size_t nbytes, int flag);
앞에서 말했던것처럼 message는 positive long int type field + non-negative length + data bytes로 구성됨.
메세지는 항상 큐의 끝에 들어감.
ptr : long integer를 가리킴. ( message type과 data를 담고 있음)
nbytes가 0이면 data는 없다.
만약 largest message가 512bytes라면 다음과 같은 스트럭쳐를 선언할수 있음.
struct mymesg {
long mtype;
char data[512];
}
ptr는 mymesg 스트럭쳐를 가리키는 포인터가 될것임. 메세지 타입은 리시버가 메세지를 가져올때 사용됨. (fifo로 가져오지 않기때문에)
flag
IPC_NOWAIT : 파일에서 논블락킹I/O와 유사. 메세지큐가 가득찼으면 EAGAIN을 리턴받을것이다.
IPC_NOWAIT가 설정되어있지 않다면, 메세지큐가 비거나 시스템에서 큐가 제거되거나, 시그널이 오기전까지 블락될것임.
메세지큐가 제거되면 EIDRM이 리턴됨.
시그널이 오면 EINTR가 리턴됨.
15.8 Semapores
세마포어는 앞에서 봤던 (pipes, FIFFos, .. ) IPC의 형태가 아님.
세마포어는 counter이다. 여러 프로세스에서 공유되는 데이터를 접근하는데 사용됨.
공유되는 자원을 얻기위해서 프로세스는 다음의 과정을 따른다.
- 리소스를 컨트롤하는 세마포어를 테스트한다.
- 세마포어의 값이 양수이면, 이 프로세스는 리소스를 사용가능. 이 경우 프로세스는 세마포어 값을 1 감소시킨다.
- 세마포어의 값이 0이면, 프로세스는 세마포어의 값이 양수가 될때까지 슬립함. 프로세스가 깨어나면 1번으로 되돌아가서 다시 시작한다.
리소스를 다 사용한 프로세스는 세마포어의 값을 1 증가시켜준다. 이때 세마포어를 기다리며 자고있는 프로세스가 있으면 깨어난다.
세마포어를 제대로 구현하기 위해서는 세마포어를 테스트하는 과정과, 세마포어를 감소시키는 과정이 atomic operation이 되어야함. 그래서 세마포어는 커널안쪽에 구현되어있다.
세마포어의 일반적인 형태는 binary semaphore다. 싱글리소스를 관리하며, 1로 초기화됨.
하지만 다 이런것은 아니며 세마포어는 모든 양수의 값으로 초기화 가능함. (리소스 수에 따른다)
불행하게도 XSI 세마포어는이것보다 복잡함. 세가지 특징이 복잡하게 만드는 원인이다.
- 세마포어가 단순히 하나의 non-negative 값이 아니다. 하나 이상의 세마포어 values set으로 정의할수 있다. 세마포어를 생성시 set 의 value들을 설정함.
- 세마포어의 생성 (semget)은 초기화(semctl)와 독립적이다. 생성과 초기화를 아토믹하게 할수없음.
- 모든 XSI IPC가 그렇듯, 사용하는 프로세스가 없어도 존재한다. 프로그램이 종료될때 혹시나 세마포어를 release하지 않고 종료될까 조심해서 짜야됨.
커널은 semid_ds 스트럭쳐를 통해 각각의 세마포어셋을 관리한다.
struct semid_ds {
struct ipc_perm sem_perm;
unsigned short sem_nsems;
time_t sem_otime; //last semop() time
time_t sem_ctime
...
};
Single Unix spec에서 정의한 필드. 각각의 세마포어는 다음 멤버들을 가짐.
struct {
unsigned short semval; // 세마포어 값
pid_t sempid; // pid for last operation
unsigned short semcnt; // # process awaiting semval > curval , 세마포어를 사용하기 위해기다리는 프로세스 수
unsigned short semzcnt; // # process awaiting semval == 0, 세마포어를 다 쓰기를 기다리는 프로세스 수
...
};
#include <sys/sem.h>
int semget(key_t key, int nsems, int flag);
//Returns: semaphore ID if OK, -1 on error.
세마포어를 사용하기 위해서는 semget을 통해 semaphore ID를 우선 얻어야함.
nsems : number of semaphores.
새로운 세마포어를 만들때는 반드시 값을 넣어줘야함.
기존에 있던 세마포어셋을 참조할때는 0을 넣는다.
#include <sys/sem.h>
int semctl(int semid, int semnum, int cmd, .... /* union semun arg */);
semid : 세마포어 ID
semnum : 세마포어 셋에서 몇번째 세마포어인지.
cmd :
- IPC_STAT : semid_ds 를 버퍼에 받아온다.
- IPC_SET : 버퍼를 semid_ds에 세팅
- IPC_RMID : 세마포어 제거
- GETVAL : semnum에 해당하는 semval 값을 얻어옴. (unix spec 참조)
- SETVAL
- GETPID
- GETNCNT
- GETZCNT
- GETALL
- SETALL
4번째 아규먼트는 optional. 커맨드에 따라 유니언 중에서 필요한 값이 달라짐.
union semun {
int val; /* for SETVAL */
struct semid_ds buf; /* for IPC_STAT and IPC_SET */
unsigned short array; /* for GETALL and SETALL */
};
#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
//Returns : 0 if OK, -1 on error
오퍼레이션 어레이를 atomic하게 실행하도록 해주는 함수이다.
struct sembuf {
unsigned short sem_num; /* member # in set */
short sem_op; /* operation (negative , 0 , positive) */
short sem_flag; /* IPC_NOWAIT, SEM_UNDO */
}
sem_op :
if sem_op > 0 : 리소스 반환. 해당값만큼 semval 증가.
if sem_op < 0 : 리소스 요청. 해당값만큼 semval 감소.
- semval이 sem_op보다 작을때 :
- IPC_NOWAIT가 켜져있으면 : EAGAIN 리턴.
- IPC_NOWAIT가 켜져있지않으면 : semncnt 1 증가시키고 sleep.
if sem_op == 0 : 세마포어 값이 0이 될때까지 기다림.
- semval이 0보다 클 때 :
- IPC_NOWAIT 가 켜져있으면 : EAGAIN 리턴.
- IPC_NOWAIT가 켜져있지 않으면 : semzcnt 1 증가시키고 sleep.
Semaphore Adjustment on exit
SEM_UNDO 플래그를 설정하고 세마포어를 얻어서 리소스를 사용하면, 세마포어를 릴리즈하지 않고 프로세스가 종료되었을때 (그게 자발적이든 아니든) 커널이 알아서 정리를 해줌.
우리가 semctl을 통해 세마포어값을 설정하면, 모든 프로세스에 있는 세마포어의 adjustment 값이 0이된다. (그래서 semctl 쓸때 조심해야 된다는건가?)
15.9 Shared Memory
메모리의 일부 영역을 여러 프로세스간에 공유할수 있도록 하는 기술.
IPC 중에서 가장 빠르다. 왜냐하면 클라이언트와 서버사이에 데이터 copy가 필요하지 않기 때문.
쉐어드 메모리의 유일한 트릭은 싱크로나이징 액세스이다.
클라이언트는 서버가 공유메모리를 사용중일때는 끝날때까지 액세스하면 안된다.
그래서 종종 세마포어는 shared memory 접근에 싱크를 맞추기 위해 사용된다. (이전 섹션에서 본거처럼 record locking 이나 뮤택스도 사용될수있다.)
mmap을 통해 멀티 프로세스가 같은 파일을 자신의 스페이스에서 접근하는걸 봤었는데, XSI Shared memory와 mmap의 차이는 접근할 수 있는 파일이 있느냐 없느냐이다. XSI Shared memory segment는 익명의 메모리 세그먼트임.
커널은 각 shared memory segment마다 다음의 스트럭쳐를 통해 관리함.
struct shmid_ds {
struct ipc_perm shm_perm;
size_t shm_segsz; // size of segment in bytes.
pid_t shm_lpid; // pid of last shmop()
pid_t shm_cpid; // pid of creator
shmatt_t shm_nattch; // number of current attaches
time_t shm_atime; // last-attach time
time_t shm_dtime; // last-detach time
time_t shm_ctime; // last-change time
.....
};
#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
//Return : shared memory ID if OK, -1 on error
size : 생성시 shared memory size (bytes) (보통 시스템 페이지사이즈의 배수로 반올림되서 사용됨)
#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf)
cmd
- IPC_STAT : shmid_ds 를 버퍼에 가져옴
- IPC_SET : 버퍼값을 shmid_ds에 적용
- IPC_RMID : shared memory 제거. 그러나 attach count가 0이될때까지 유지됨. 마지막 프로세스가 터미네이트되거나 detach할때까지 지워지지않고 유지된다.
shared memory가 남아있더라도, identifier는 바로 지워지며, shmat은 더 이상 동작하지 못함.
스펙은 아니지만 리눅스와 솔라리스에서 추가로 제공하는 커맨드.
- SHM_LOCK : 공유메모리공간을 락. 슈퍼유저만 사용가능.
- SHM_UNLOCK : 공유메모리공간을 언락. 슈퍼유저만 사용가능.
#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
//returns : pointer to shared memmory segments if OK, -1 on error
if addr == 0 : 커널에 의해 선택된 첫번째 사용가능한 어드레스에 세그먼트가 attach 됨. (추천방식)
if addr != 0 && no SHM_RND : 세그먼트는 지정된 addr에 attach된다.
if addr != 0 && SHM_RND :
(addr - (addr modulus SHMLBA))에 attach 된다.
SHM_RND : round
SHM_LBA : low boundary address multiple (2^n)
shmat이 성공하면 커널은 shm_nattch 카운트를 늘린다.
flag
- SHM_RDONLY : 세그먼트가 read-only로 attach됨.
#include <sys/shm.h>
int shmdt (const void *addr);
//Returns : 0 if OK, -1 on error.
shared memory를 다 쓰고나면 이 함수를 써서 detach한다. 이걸 부른다고해서 id나 관련된 데이터스트럭쳐가 지워지지 않는다. (지우려면 shmctl 의 IPC_RMID를 써야한다.)
addr은 shmat에서 리턴받은 값.
15.10 POSIX Semaphore
POSIX 세마포어는 POSIX.1의 real-time extension에서 생겨난 세가지 IPC 메커니즘 중 하나임.
Single UNIX spec에선 세가지 메커니즘 (message queue, semaphores, shared message)이 옵션이었음.
SUSv4에서는 base spec으로 이동. 그러나 message queue, shared memory는 여전히 옵션으로...
POSIX 세마포어는 XSI 세마포어의 몇가지 결함을 해결하기 위한 인터페이스임.
- 속도 (higher performance)
- 쓰기쉬움 (no semaphore sets. file system operation과 비슷한 인터페이스.
- more gracefully when removed. XSI 세마포어에서 지워진 세마포어에 접근할때 EIDRM errno을 받았는데, POSIX 세마포어에서는 마지막 레퍼런스를 가진녀석의 work가 끝날때까지 기다렸다가 릴리즈됨.
두가지 방식으로 사용가능 : named / unnamed
두가지 방식은 create/destroy할때만 다르고 나머진 똑같이 동작.
언네임드 세마포어는 메모리에만 존재하며, 세마포어를 사용하기 위해서는 프로세스가 메모리에 접근해야만 함.
즉, 1) 같은 프로세스에 있는 쓰레드거나
2) 다른프로세스에 있는 쓰레드의 경우에는 자신의 address space에 같은 메모리 영역을 매핑해야지만
쓸 수 있다.
네임드 세마포어의 경우엔 name으로 접근하기때문에 어떤 프로세스에 있는 쓰레드든 이름만 알고있다면 사용가능.
#include <semaphore.h>
sem_t sem_open(const char name, int oflag, .... /*mode_t mode, unsigned int value */);
//returns: ptr to semaphore if OK, SEM_FAILED on errror
새로운 세마포어를 만들거나, 기존의 세마포어를 사용하기 위해서는 이 함수를 써야함.
이미 만들어져있는 네임드 세마포어를 사용하기 위해서는 아규먼트 2개만 쓰면 됨.
- 세마포어의 이름(name),
- 0 (oflag)
oflag에 O_CREAT를 주면 세마포어가 존재하지 않을때 세마포어를 새롭게 만든다는뜻. (이미 있으면 걔를 얻어옴)
O_CREAT를 쓸땐 두가지 아규먼트를 더 설정 해줘야함.
- mode : 권한 (user-read, user-write, user-execute,...) (파일을 열때랑 똑같음)
- value : 세마포어의 초기 값. (0 ~ SEM_VALUE_MAX).
무조건 세마포어를 새롭게 만들도록 보장하고 싶다면, O_CREATE | O_EXCL 같이 쓰면됨. 세마포어가 이미 존재하면 함수는 fail.
이식성을 높이기 위해서 우리는 세마포어의 이름을 정할때 몇가지 컨벤션을 따라야함.
- 네임의 첫번째 글자는 '/'이어야함. POSIX 세마포어가 파일 시스템을 사용해야한다는 요구사항은 없지만, 만약 파일 시스템이 사용된경우에는, name의 스타팅 포인트가 어디인지 애매하지 않도록 하고 싶음. (네임을 절대주소로 써서 찾기 쉽게하자)
- 슬래시는 저거 하나만 있어야함. 그래야 구현에 따라 동작을 다르게 하지 않게 됨. 예를들어 파일시스템이 사용된 경우, /mysem 과 //mysem은 같은 파일네임을 가리키게됨. 그러나 파일시스템을 사용하지 않은 경우엔 두개가 다른걸로 인식될것임. (예를들어 해쉬를사용한경우라던지..)
- 네임의 max length는 구현에 따라 다름. _POSIX_NAME_MAX를 넘지 않도록 하자. 왜냐면 이게 파일 시스템을 이용한 구현의 경우 minimum acceptable limit이기 때문.
#include <semaphore.h>
int sem_close(sem_t *sem);
//returns: 0 if OK, -1 on error
위에서 오픈한 다 쓰고나서 세마포어를 닫을때 사용. (리소스를 릴리즈)
만약 우리가 sem_close를 호출하는걸 까먹고 프로세스를 종료한 경우, 커널이 자동으로 세마포어를 close해줌.
그러나 이것은 세마포어의 state에는 영향을 주지 않는다.
즉, 우리가 세마포어 값을 증가시키고 나간경우, 그 값이 다시 줄어들지는 않음.
비슷하게, 우리가 sem_close를 호출해도, 세마포어의 값은 영향을 받지 않는다.
이런점에서 XSI 세마포어에서 SEM_UNDO 플래그와는 다름.
네임드 세마포어를 destroy 하기 위해서는 sem_unlink를 호출.
#include <semaphore.h>
int sem_unlink(const char* name);
//returns: 0 if OK. -1 on error.
이걸 호출하면 세마포어의 네임을 지움.
만약 세마포어를 오픈하고있는 애가 아무도 없으면, destroy됨. 마지막 레퍼런스를 가진녀석이 close될때까지 destruction은 연기됨.
XSI 세마포어와 달리, POSIX 세마포어는 하나의 함수로 세마포어의 값을 변경시킬 수 있음.
locking과 decrementation이 하나로됨.
#include <semaphore.h>
int sem_trywait(sem_t *sem);
int sem_wait(sem_t *sem);
//returns : 0 if OK, -1 on error.
세마포어 값을 감소시키기 위해서는 이 두가지 함수를 쓰면됨.
sem_wait는 세마포어 값이 0이면 블락됨.
세마포어값을 감소시키거나 시그널에 의해 인터럽트되기전까지 리턴되지않음.
sem_trywait를 쓰면 논블락킹임.
세마포어값이 0이면 -1리턴. + EAGAIN errno 설정.
timewait라는 것도 있다.
#include <semaphore.h>
#include <time.h>
int sem_timedwait(sem_t *restrict sem, const struct timespec *restrict tsptr);
//returns : 0 if OK, -1 on error
일정시간만큼만 기다리고 싶을때 사용.
timeout은 CLOCK_REALTIME 베이스다 (Figure6.8 참고)
timeout될경우 -1 리턴 + ETIMEOUT errno 설정.
세마포어값을 증가시키고 싶을땐 이거사용.
#include <semaphore.h>
int sem_post(sem_t *sem);
//retunrs : 0 if OK, -1 on error.
어떤 프로세스가 wait에 의해 블락되어 있을 때, 우리가 sem_post를 콜해주면 그 프로세스는 깨어나게됨.
싱글프로세스에서 POSIX 세마포어를 사용할땐 unnamed 세마포어를 사용하는게 더쉬움. (당연?)
그래서 언네임드 세마포어를 만드는 방법을 소개.
#include <semaphore.h>
int sem_init(sem_t *sem, int pshared, unsigned int value);
//returns: 0 if OK, -1 on error.
pshared는 우리가 세마포어를 멀티프로세스용으로 쓸것인지를 나타냄. (멀티용으로 쓰고싶으면 nonzero로 set)
ptr을 리턴하는 sem_open과 달리 이거는 sem_t를 어딘가에 declare해놓고, 아규먼트로 pass해서 사용한다.
언네임드 세마포어 버리는법.
#include <semaphore.h>
int sem_destroy(sem_t *sem);
//returns : 0 if OK, -1 on error.
이거 호출 후에는 다른 세마포어 함수 전부 사용 X. (sem_init 빼고)
#include <semaphore.h>
int sem_getvalue(sem_t *restrict sem, int *restrict valp);
//returns : 0 if OK, -1 on error.
세마포어의 밸류를 얻는 함수.
그러나 알다시피 우리가 이걸 읽어들이고 나면 값이 바뀌어 있을 수도 있음.
추가적인 싱크로나이즈 메커니즘을 같이 사용하지 않을것이라면 이 함수는 디버깅용으로만 써라.
15.11 Client-Server Porperties
skip..
15.12 Summary
skip...
'스터디 > APUE' 카테고리의 다른 글
9. Process RelationShips (0) | 2017.12.28 |
---|---|
5. Standard I/O Library (0) | 2017.12.28 |