🔔 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다. 혹여나 틀린 부분이 있다면 지적해주시면 감사드리겠습니다.
#include <sys/select.h>
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
struct timeval *timeout);
select
는 program이 여러 개의 file descriptor를 모니터링하고, 하나 이상의 file descriptor가 I/O operation에 대해 ready
될 때까지 기다린다.
여러 개의 file descriptor
의 변화를 주목하는 역할로, readfds
, writefds
, exceptfds
3가지의 bit mask가 존재한다. 각 bit mask의 bit는 하나의 file descriptor를 의미하며, 1
로 설정되어있는 것을 판별한다.
ndfs
는 가장 큰 file descriptor number + 1
에 해당하며, timeout
은 file descriptor가 준비될 때까지 block되는 간격을 지정한다. 해당 시간이 지나면 block을 멈추고 return하게 되며, NULL
로 값을 지정할 경우 blocking을 유지한다.
Return
- 각 3가지의 descriptor set의 준비된 file descriptor의 수를 return
- error 발생 시
return -1
readfds
, writefds
, exceptfds
는 1024bit
의 data-type으로, 아래와 같다.
select
가 return되면, 준비된 file descriptor를 1
로 설정하여 나타낸다. loop 내에서 select
를 사용하는 경우에는 매번 set을 다시 초기화할 필요가 있으며, 이는 아래의 방법으로 설정하거나 초기화가 가능하다.
void FD_ZERO(fd_set *fdset);
void FD_SET(int fd, fd_set *fdset);
int FD_ISSET(int fd, fd_set *fdset);
void FD_CLR(int fd, fd_set *fdset);
Functions for setting fd_set
FD_ZERO
: bit-mask를 0으로 설정FD_SET
: 전달받은 fd를 1로 지정FD_ISSET
: 1로 변경되어있는지 확인
- 이를 이용하여 fd가 ready인지 확인 가능FD_CLR
: 전달받은 fd를 0으로 설정
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <fcntl.h>
int main() {
...
int fd1, fd2;
fd_set readset;
fd1 = open(“file1”, O_RDONLY);
fd2 = open(“file2”, O_RDONLY);
FD_ZERO(&readset);
FD_SET(fd1, &readset);
FD_SET(fd2, &readset);
switch(select(5, &readset, NULL, NULL, NULL)) {
/* codes */
}
...
}
위 코드는 readset
을 0으로 초기화한 이후, fd1과 fd2를 설정하여 select를 수행하는 코드이다. 이를 그림으로 나타내면 아래와 같다.
#include <sys/select.h>
#include <sys/time.h>
#include <sys/wait.h>
#define MSGSIZE 6
char *msg1 = “hello”;
char *msg2 = “bye!!”;
void parent(int[][]);
void child(int[]);
int main() {
int pip[3][2];
for (int i=0; i<3; i++) {
if (pipe(pip[i]) == -1)
fatal(“pipe call”);
switch(fork()) {
case -1:
fatal(“fork call”);
case 0:
child(pip[i]);
}
}
parent(pip);
return 0;
}
void parent(int p[3][2]) {
char buf[MSGSIZE], ch;
fd_set set, master;
for (int i=0; i<3; i++) close(p[i][1]);
FD_ZERO(&master);
FD_SET(0, &master);
for (int i=0; i<3; i++)
FD_SET(p[i][0], &master);
while (set=master, select(p[2][0]+1,&set,NULL,NULL,NULL)>0) {
if (FD_ISSET(0, &set)) {
printf(“From standard input...”);
read(0, &ch, 1);
printf(“%c\n”, ch);
}
for (int i=0; i<3; i++) {
if (FD_ISSET(p[i][0], &set)) {
if (read(p[i][0], buf, MSGSIZE) > 0) {
printf(“Message from child%d\n”, i);
printf(“MSG=%s\n”, buf);
}
}
}
if (waitpid(-1, NULL, WNOHANG) == -1) return;
}
}
void child(int p[2]) {
close(p[0]);
for (int count=0; count<2; count++) {
write(p[1], msg1, MSGSIZE);
sleep(getpid()%4); /* random time sleep */
}
write(p[1], msg2, MSGSIZE);
exit(0);
}
위 코드는 pipe
를 3번 호출하여 pipe를 총 3개를 생성한 이후, fork
역시 3번 실행해 각 child는 child
를, parent는 parent
를 실행한다.
child
는 hello
를 2번 pipe에 write 이후 bye!!
를 write하고 종료하, parent
는 3개의 child와 연결된 pipe를 select
를 이용해 모니터링하여 각 file descriptor가 준비되었을 경우, read하여 해당 내용을 출력해준다. 아래가 그 실행 결과와 과정을 나타낸 것이다.
Message from child 0
MSG = hello
Message from child 1
MSG = hello
Message from child 2
MSG = hello
d
From standard input d
From standard input
Message from child 0
MSG = hello
Message from child 1
MSG = hello
Message from child 2
MSG = hello
Message from child 0
MSG = bye!!
// -> waitpid가 while 안에 있으므로
// 하나가 종료되면 나머지도 종료되므로 여기까지만 출력됨
#include <stdio.h>
int main() {
char *one[4] = {“ls”, “-l”, “/usr/lib”, NULL};
char *two[3] = {“grep”, “^d”, NULL};
int ret;
ret = join(one, two);
printf(“join returned %d\n”, ret);
return 0;
}
int join (char *com1[], char *com2[]) {
int p[2], status;
switch (fork()) {
case -1:
fatal(“1st fork call in join”);
case 0:
break;
default:
wait(&status);
return status;
}
if (pipe(p) == -1)
fatal(“pipe call in join);
switch (fork()) {
case -1:
fatal(“2nd fork call in join”);
case 0:
dup2(p[1],1);
close(p[0]);
close(p[1]);
execvp (com1[0], com1);
fatal(“1st execvp call in join”);
default:
dup2(p[0], 0);
close(p[0]);
close(p[1]);
execvp(com2[0], com2);
fatal(“2nd execvp call in join”);
}
}
a.out | b.out
과 같이 pipe
를 이용하여 join을 구현한 간단한 예시 코드로, 각각 p[1]
을 standard output
으로, p[0]
를 standard in
으로 지정하여 command를 실행하는 코드이다. 이를 그림으로 나타내면 아래와 같다.
FIFO
는 named pipes
로, parent, child와 같이 서로 연관이 있는 process 사이에서 사용될 수 있는 pipe
와는 달리, 연관되지 않은 서로 다른 process간에 사용할 수 있는 pipe
이다. 또한 pipe
와는 달리 2개로 나누어 관리되는 것이 아닌, 하나를 통해 통신하고 관리된다.
FIFO
는 owner
, size
, permission
이 존재하며, 하나의 file로 관리되고 밖에서 접근이 가능하다. 다른 file들처럼 open, close, delete가 가능하며, FIFO
를 생성하는 command는 아래와 같다.
$ /etc/mknod channel p
$ ls –l
prw-rw-r-- 1 ben usr 0 Aug 1 21:05 channel
위 command는 channel
이라는 이름의 FIFO
를 type p
로 생성한다는 의미이다.
FIFO
에 write
를 하면 가장 뒤에 data가 추가되며, read
는 항상 FIFO
의 시작 지점부터 시작된다. lseek
를 사용할 수 없으며, error ESPIPE
를 return한다.
# include <sys/stat.h>
int mkfifo(const char *pathname, mode_t mode);
mkfifo
는 FIFO
를 생성한다.
Return
- 성공 시
return 0
- error 발생 시
return -1
Arguments
pathname
: FIFO file namemode
: permission
FIFO
는 open
으로 열 수 있으며, flag 설정에 따라 그 동작에 차이가 있다.
O_NONBLOCK 미설정
O_RDONLY
일 경우 다른 process에서writing
을 위해 open할 때까지 blockO_WRONLY
일 경우 다른 process에서reading
을 위해 open할 때까지 block
O_NONBLOCK 설정
O_RDONLY
일 경우, open을 해두고 바로 returnO_WRONLY
일 경우reading
으로 open한 process가 있으면 바로 return하나, 존재하지 않으면errno = ENXIO
와 함께return -1
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#define MSGSIZE 63
char *fifo = “fifo”;
int main(int argc, char **argv) {
int fd, nwrite;
char msgbuf[MSGSIZE+1];
if (argc < 2) {
fprintf (stderr, “Usage: sendmessage msg...\n”);
return 1;
}
if ((fd = open(fifo, O_WRONLY | O_NONBLOCK)) < 0)
fatal(“fifo open failed”);
for (int i=1; i<argc; i++) {
if (strlen(argv[j]) > MSGSIZE) {
fprintf(stderr, “message too long %s\n”, argv[j]);
continue;
}
strcpy(msgbuf, argv[j]);
if ((nwrite = write(fd, msgbuf, MSGSIZE+1) == -1)
fatal(“message write failed”);
}
return 0;
}
#include <fcntl.h>
#include <stdio.h>
#include <errno.h>
#define MSGSIZE 63
char *fifo = “fifo”;
int main(int argc, char **argv) {
int fd;
char msgbuf[MSGSIZE+1];
if (mkfifo(fifo, 0666) == -1) {
if (errno != EEXIST) fatal (“receiver: mkfifo”);
}
if ((fd = open(fifo, O_RDWR)) < 0)
fatal(“fifo open failed”);
for(;;) {
if (read(fd, msgbuf, MSGSIZE+1) < 0)
fatal(“message read failed”);
printf(“message received:%s\n”, msgbuf);
}
return 0;
}
write process
의 경우 O_NONBLOCK
이 설정되었으므로, read process
가 먼저 실행되어야 하며, read process
의 경우 O_RDWR
로 open하였다.
이는 내용을 다 읽었을 경우, read는 0을 return하기 때문에 의미없는 내용을 계속 반복하여 출력하게 되는데, O_RDWR
로 open하면, write process
가 종료되어도 writer
가 하나 존재하게 되므로, block되어 무의미한 출력을 방지하게 된다. 이를 위해 O_RDWR
로 open해준 것이다.
항공사의 서로 다른 사무실에서 좌석 예약을 제공한다고 가정하자. 만약 좌석이 1개만 남아있을 경우에 두 사무실에서 거의 동시에 예약을 하게 되면, 1개의 좌석에 대해 2개의 예약이 진행될 수도 있다.
이를 위해서 한 곳에서 작업을 하고 있을 때, 다른 곳에서 접근하지 못하도록 lock하는 것이 필요하며, 이것이 Record Locking
이라고 할 수 있다.
두 사람이 동시에 같은 file을 편집하는 경우, file의 최종 상태는 마지막 process에 해당된다. 하지만 DB
같은 경우에는 file에만 기록을 하는 경우도 존재하므로, 이를 위해 UNIX
에서는 file
에 대한 Record Locking
을 제공한다.
이는 file 전체를 막는 것이 아니라 수정하는 부분에 대해서 lock하는 것이며, 이는 fnctl
로 접근이 가능하다.
Advisory Locking
- 일관된 방식으로 record locking을 처리함
- kernel이 관여하지 않으며, application process level에서 lock 여부를 확인해야 함
- lock을 했다고 해서 접근이 불가능한 것은 아니며, 이를 따로 처리해주어야 함
- lock 여부를 check하고 실행해야 문제없이 동작하므로 check 해야함
Mandatory Locking (의무적)
- kernel(system에 의해 관리됨)에 의해 process가 lock된 file에 접근하려는지 확인
- 일반적인 file descriptor에 대해서는 lock이 풀릴때까지 block
- lock이 release되면 block이 풀리고 process 다시 진행
- Mandatory의 경우
Dead Lock
발생 가능
#include <fcntl.h>
int fcntl(int filedes, int cmd, struct flock *ldata);
Return
- 성공 시 return value는
cmd
에 따라 달라짐- error 발생 시
return -1
Arguments
filedes
: file descriptorcmd
-F_GETLK
: lock status를 get, lock되어있는지를 check하며, 1byte라도 lock되어있으면, 존재하는 lock으로ldata
가overwritten
되며, lock이 없을 경우,l_type = F_UNLCK
으로 변경하여 return
-F_SETLK
: lock을 set,F_SETLK
가 불가능하면,errno = EACCES or EAGAIN
과 함께return -1
-F_SETLKW
: lock을 set 하나 될때까지 blockingldata
: lock description을 저장
struct flock {
short l_type;
off_t l_start;
short l_whence;
off_t l_len;
pid_t l_pid;
}
struct flock
는 위와 같으며 각각 아래와 같은 의미를 가진다.
struct flock
l_type
:F_RDLCK
은 read lock,F_WRLCK
은 write lock,F_UNLCK
은 unlock을 의미l_start
: lock을 시작할 위치를 지정l_whence
:SEEK_SET
,SEEK_CUR
,SEEK_END
로 어디서부터 시작할지에 대한 기준을 설정l_len
: 얼마나 lock할건지 지정l_pid
: 누가 lock했는지에 대해 저장
lock은 end of file
을 넘어서서 지정하는 것이 가능하지만, file의 시작보다는 앞서서 설정이 불가능하다. l_len = 0
일 경우에는 시작 지점부터 가능한 끝까지 확장됨을 의미하며, 이를 이용하여 아래와 같은 방식으로 전체 file을 lock할 수 있다.
lock entire file
l_start = 0
l_whence = SEEK_SET
l_len = 0
F_RDLCK
은 shared lock이고, F_WRLCK
은 exclusive lock이며, 이는 read에 대한 lock이 설정되었을 경우에, 다른 process에서 read에 대한 lock이나 unlock이 가능하나, write에 대한 lock이 설정되었을 경우에는 unlock을 제외하고 나머지가 불가능하기 때문이다.
#include <unistd.h>
#include <fcntl.h>
...
struct flock my_lock;
my_lock.l_type = F_WRLCK;
my_lock.l_whence = SEEK_CUR;
my_lock.l_start = 0;
my_lock.l_len = 512;
fcntl(fd, F_SETLKW, &my_lock);
F_SETLKW
이므로 lock이 될때까지 block(sleeping)되며, 이는 unlock될 때 발생하는 signal에 의해 interrupt된다.
process 종료 시 설정된 모든 lock
은 release되며, 이는 해당 file descriptor가 닫혀도 동일하게 release된다.
fork
에 의해 유전되지는 않으나, exec
의 경우는 process가 변한 것은 아니기 때문에 lock
이 유지된다. 단 close-on-exec-flag
의 경우 exec
과 함께 file descriptor를 close하기 때문에 이 경우에는 release된다.
#include <unistd.h>
#include <fcntl.h>
#include <stdlib.h>
int main() {
int fd;
struct flock my_lock;
my_lock.l_type = F_WRLCK;
my_lock.l_whence = SEEK_SET;
my_lock.l_start = 0;
my_lock.l_len = 10;
fd = open(“locktest”, O_RDWR);
if (fcntl(fd, F_SETLKW, &my_lock) == -1) {
perror(“parent: locking”);
return 1;
}
printf(“parent: locked record\n”);
switch (fork()) {
case -1:
perror(“fork”);
exit(1);
case 0:
my_lock.l_len = 5;
if(fcntl(fd, F_SETLKW, &my_lock) == -1) {
perror(“child: locking”);
return 1;
}
printf(“child:locked and exiting\n”);
return 0;
}
sleep(5);
printf(“parent: exiting\n”);
return 0;
}
위의 경우는 fork
를 통해 parent가 lock
한 부분과 겹치는 부분을 child에서 F_SETLKW
로 lock
하는 코드로, 범위가 겹치기 때문에 parent가 종료되어 lock이 release될 때까지 block된다.
여기서 my_lock
의 경우, lock
이 유전되지 않는 것이지 이는 child에서도 동일하게 사용이 가능하다. 위와 같이 종료료 release하는 것이 아닌, 중간에 release를 위해서는 아래와 같이 실행하면 된다.
printf(“parent: unlocking\n”);
my_lock.l_type = F_UNLCK;
if (fcntl(fd, F_SETLK, &my_lock) == -1) {
perror(“parent: unlocking”);
return 1;
}
lock
된 범위 중간의 작은 범위만 unlock
한 경우, system은 2개로 나누어져 관리된다. 이는 아래와 같다.
#include <unistd.h>
#include <stdio.h>
#include <errno.h>
...
if (fcntl(fd, F_SETLK, &a_lock) == -1) {
if (errno == EACCES || errno == EAGAIN) {
fcntl(fd, F_GETLK, &b_lock);
fprintf(stderr, “record locked by %d\n”, b_lock.l_pid);
}
else
perror(“unexpected lock error”);
}
위와 같이 어떠한 process가 lock되었는지 F_GETLK
를 이용하여 알 수 있다.
Dead lock
은 위와 같이 이미 각각의 process에서 서로 다른 부분에 lock
을 한 이후에, 서로가 lock한 부분에 대해 lock
을 진행하려는 경우에 발생한다. F_SETLKW
의 경우 lock
이 가능할때까지 block되기 때문에, 무한히 block되는 것을 방지하기 위해 errno = EDEADLK
와 함께 return -1
을 한다.
이러한 dependency를 무한히 check하는 것은 아니며, 대략 10개까지 이를 check한다.