리눅스 프로그래밍 - 12주차

Lellow_Mellow·2022년 11월 27일
1
post-thumbnail

🔔 학교 강의를 바탕으로 개인적인 공부를 위해 정리한 글입니다. 혹여나 틀린 부분이 있다면 지적해주시면 감사드리겠습니다.

System Call : select

#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, exceptfds1024bit의 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으로 설정

Example : select

#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를 실행한다.

childhello를 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 안에 있으므로 
// 		하나가 종료되면 나머지도 종료되므로 여기까지만 출력됨

Example : join(my_own_pipe)

#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

FIFO

FIFOnamed pipes로, parent, child와 같이 서로 연관이 있는 process 사이에서 사용될 수 있는 pipe와는 달리, 연관되지 않은 서로 다른 process간에 사용할 수 있는 pipe이다. 또한 pipe와는 달리 2개로 나누어 관리되는 것이 아닌, 하나를 통해 통신하고 관리된다.

FIFOowner, 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이라는 이름의 FIFOtype p로 생성한다는 의미이다.

FIFOwrite를 하면 가장 뒤에 data가 추가되며, read는 항상 FIFO의 시작 지점부터 시작된다. lseek를 사용할 수 없으며, error ESPIPE를 return한다.

System Call : mkfifo

# include <sys/stat.h>

int mkfifo(const char *pathname, mode_t mode);

mkfifoFIFO를 생성한다.

Return

  • 성공 시 return 0
  • error 발생 시 return -1

Arguments

  • pathname : FIFO file name
  • mode : permission

FIFOopen으로 열 수 있으며, flag 설정에 따라 그 동작에 차이가 있다.

O_NONBLOCK 미설정

  • O_RDONLY일 경우 다른 process에서 writing을 위해 open할 때까지 block
  • O_WRONLY일 경우 다른 process에서 reading을 위해 open할 때까지 block

O_NONBLOCK 설정

  • O_RDONLY일 경우, open을 해두고 바로 return
  • O_WRONLY일 경우 reading으로 open한 process가 있으면 바로 return하나, 존재하지 않으면 errno = ENXIO와 함께 return -1

Example : FIFO

send message

#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;
}

receive message

#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해준 것이다.

Advanced Inter-Process Communications : Record Locking

Record Locking

Motivation

항공사의 서로 다른 사무실에서 좌석 예약을 제공한다고 가정하자. 만약 좌석이 1개만 남아있을 경우에 두 사무실에서 거의 동시에 예약을 하게 되면, 1개의 좌석에 대해 2개의 예약이 진행될 수도 있다.

이를 위해서 한 곳에서 작업을 하고 있을 때, 다른 곳에서 접근하지 못하도록 lock하는 것이 필요하며, 이것이 Record Locking이라고 할 수 있다.

Record Locking

두 사람이 동시에 같은 file을 편집하는 경우, file의 최종 상태는 마지막 process에 해당된다. 하지만 DB 같은 경우에는 file에만 기록을 하는 경우도 존재하므로, 이를 위해 UNIX에서는 file에 대한 Record Locking을 제공한다.

이는 file 전체를 막는 것이 아니라 수정하는 부분에 대해서 lock하는 것이며, 이는 fnctl로 접근이 가능하다.

Advisory vs Mandatory Locking

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 발생 가능

Record Locking : fcntl

#include <fcntl.h>

int fcntl(int filedes, int cmd, struct flock *ldata);

Return

  • 성공 시 return value는 cmd에 따라 달라짐
  • error 발생 시 return -1

Arguments

  • filedes : file descriptor
  • cmd
    - F_GETLK : lock status를 get, lock되어있는지를 check하며, 1byte라도 lock되어있으면, 존재하는 lock으로 ldataoverwritten되며, lock이 없을 경우, l_type = F_UNLCK으로 변경하여 return
    - F_SETLK : lock을 set, F_SETLK가 불가능하면, errno = EACCES or EAGAIN과 함께 return -1
    - F_SETLKW : lock을 set 하나 될때까지 blocking
  • ldata : 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을 제외하고 나머지가 불가능하기 때문이다.

Example : fcntl

#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된다.

Lock Inheritance

process 종료 시 설정된 모든 lock은 release되며, 이는 해당 file descriptor가 닫혀도 동일하게 release된다.

fork에 의해 유전되지는 않으나, exec의 경우는 process가 변한 것은 아니기 때문에 lock이 유지된다. 단 close-on-exec-flag의 경우 exec과 함께 file descriptor를 close하기 때문에 이 경우에는 release된다.

Example : Lock Inheritance

#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_SETLKWlock하는 코드로, 범위가 겹치기 때문에 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;
}

Releasing Lock

lock된 범위 중간의 작은 범위만 unlock한 경우, system은 2개로 나누어져 관리된다. 이는 아래와 같다.

Example : Lock Test

#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를 이용하여 알 수 있다.

Deadlock

Dead lock은 위와 같이 이미 각각의 process에서 서로 다른 부분에 lock을 한 이후에, 서로가 lock한 부분에 대해 lock을 진행하려는 경우에 발생한다. F_SETLKW의 경우 lock이 가능할때까지 block되기 때문에, 무한히 block되는 것을 방지하기 위해 errno = EDEADLK와 함께 return -1을 한다.

이러한 dependency를 무한히 check하는 것은 아니며, 대략 10개까지 이를 check한다.

profile
festina lenta

0개의 댓글