본문 바로가기

CS/유닉스프로그래밍

13. 유닉스 IPC - semaphore, shared memory

The semop(2) system call

#include <sys/sem.h>
int semop(int semid, struct sembuf semoparray[], size_t nops);
// returns : 0 if ok, -1 on error

semop 세마포어의 오퍼레이션 인데 두개가 있음 wait(p) signal(v)
이 두 개의 오퍼레이션을 semop이라는 시스템콜로 구현을 함


첫번째 semid가 가리키는 세마포어 set이 있고 그 안에 세마포어가 여러 개 있음
semop 함수는 atomic함 id로 가리키는 세마포어 set에 여러 개 sem이 있는데

각 sem에 대해서 wait나 signal 오퍼레이션 수행하는데 그게 여러개 있음. 그것들을 각각 atomic 하게 실행함


wait - signal 세마포어가 같은 거에 대해서는 wait나 signal이나 interleave할 수 없음 
wait가 한번 실행되면 끝날때까지 다른 wait나 다른 signal이 실행될 수 없음
wait가 실행될때 wait만 atomic 하지 않고 같은 세마포어에 대해서 wait와 signal 도 같이 atomic 함

 
세마포어가 여러 개 있는데 가각 세마포어에 대해서 operation을 지정해주고 그 operation을 동시에 atomic 하게 실행 그 operation을 나타내는 변수가 있어야하는데 그게 이제 array로 들어감 semoparray임 
각 element 마다 특정 세마포어의 operation을 지정하는거임


nops는 array에서 지정된 세마포어가 총 몇 개냐는 거임 여기서 주의해야할 거는 id가 가리키는 sem의 갯수가 
예를 들어서 다섯 개라도 array가 모두 다섯 개이지 않아도 됨 5개보다 작을 수 있음

예를 들어 3개라하면 nops는 3이 됨. 그 3개에 대해서 오퍼레이션을 수행하는거임 

sem buf를 찾아가보면 이런 자료구조로 되어 있음 


첫번째 멤버 sem_num : sem set에 있는 몇 번째 sem을 가리키고 있는 지를 나타내는 거임
두번째 멤버 sem_op : 특정 세마포어 번호에 대해서 wait나 signal을 해야하는데 그걸 지정하는 변수
세번째 멤버 flag : ipc_nowait나 sem_undo

 


sem_op이 0보다 큰 수일 경우에 v signal operation 수행
sem_op을 semval에 더해줌, 세마포어의 값은 리소스의 갯수임 이렇게 생각하는게 일반적임 

 

critical section에서 그걸 수행하는 프로세스 갯수를 리소스라하면 한 개밖에 안됨 

누가 이 리소스를 확보하면 더이상 없어서 다른 프로세스가 들어와서 실행할 수 없음

임계영역 실행하려면 리소스를 확보해야하는거임 

 

음수라면 p, wait를 수행함 
0 이라면 테스트할 때만 사용하고 실제로 사용하지 않음 
ipc_nowait는 기다릴 때 기다리지 않는거고 sem_undo는 나중에 설명

 

 

void setsembuf(struct sembuf *s, int num, int op, int flg) {
   s->sem_num = (short)num;
   s->sem_op = (short)op;
   s->sem_flg = (short)flg;
   return;
}

struct sembuf myop[3];

setsembuf(myop[0], 1, -1, SEM_UNDO);  
setsembuf(myop[1], N,  2, SEM_NOWAIT);  
setsembuf(myop[2], 2,  0, SEM_UNDO);

if (semop(semid, myop, 3) == -1)
  perror(“Failed to perform semaphore operation”);

 

 

sembuf라는 게 오퍼레이션을 나타내는 구조체임 그리고 num은 몇번째 세마포어를 말하는지
그 세마포어의 오퍼레이션이 op 양수면 signal 음수면 wait임 

그리고 flag가 주어짐 그렇게 s에 저장하는 거임


오퍼레이션 세 개를 준비해서 각각에 대해서 오퍼레이션 0 1 2 총 세개의 오퍼레이션을 각 setsembuf에 assign함
1번 세마포어 넘버에 wait(1) n번에 대해서 signal(n) 2번 세마포어는 테스트 하는거임 test(2)
각각의 세마포어에 대해서 operation 지정하고 이 오퍼레이션을 동시에 실행할 때 
sem_op하고 id 지정하고 3개 넣어서 가면 됨 그럼 각각 atomic 하게 실행됨

 

#include <sys/types.h>  /* 세마포 예의 헤더 화일 “pv.h” */
#include <sys/ipc.h>  
#include <sys/sem.h>  
#include <errno.h>
#define SEMPERM 0600
#define TRUE    1
#define FALSE   0

typedef union semun {
    int val;
    struct semid_ds *buf;
    ushort *array;
 } semun;

 

int initsem (key_t semkey){
 int status = 0, semid;
 if ((semid = semget (semkey, 1, SEMPERM | IPC_CREAT |IPC_EXCL)) == -1){
   if (errno == EEXIST) semid = semget (semkey, 1, 0);
 }
 else{   /* 만일 생성되었으면 ... */
  semun arg;  
  arg.val = 1;
  status = semctl(semid, 0, SETVAL, arg);
}
 if (semid == -1 || status == -1){
   perror ("initsem failed");
   return (-1);
 }
 return (semid); /* 모든 것이 잘되었음. */
}

 

critical section 의 앞에 wait(s) 뒤에 signal(s)로 임계영역에 프로세스 하나만 들어오게 하는 것이 mutual exclusion


semperm은 600 

semctl에 id semno cmd 하고 네번째에 들어가는 내용이 semun임 
네번째 argument 사용되는 변수가 cmd에 따라서 셋 중에 하나가 지정됨 


key값을 주고 세마포어를 가져오는데 semget해서 key값을 넣어줌 1은 세마포어 갯수임 1개라는 말임
semperm은 아까 정의해놓은거 ipc_creat ipc_excl key가 있으면 실패하고 없으면 만드는 거임
있는지 확인한 거가 if 안에 있는 if문 있으면 sem key를 가져오는거임 그게 안에 있는 if문
처음 만들었을 때 else로 옴 그럼 그 세마포어에 초기값을 setting 해줘야함


그래서 sem un에 value를 쓰고 ctl에 setval 해서 argument는 초기값을 나타냄 

그때 0번째 세마포어에 대해서 초기값을 1로 설정해주고 critical section에 들어갈 세마포어의 초기값을 1로 설정한거임
근데 이게 실패했으면 존재해서 실패했다면 그 key를 불러오는 거임 
그렇게 semid를 얻었는데 실패하면 error고 얻었으면 return 해줌

 

/* p.c – semaphore p operation */
#include "pv.h"

int p (int semid){ /* p.c -- 세마포 p 연산 */
 struct sembuf p_buf;
 p_buf.sem_num = 0;
 p_buf.sem_op = -1;
 p_buf.sem_flg = SEM_UNDO;
 if (semop(semid, &p_buf, 1) == -1){
   perror ("p(semid) failed");
   exit (1);
 }
 return (0);
}

 

/* v.c – semaphore v operation */ 
#include "pv.h"

int v (int semid){ /* v.c -- 세마포 v 연산 */
 struct sembuf v_buf;
 v_buf.sem_num = 0;
 v_buf.sem_op = 1;
 v_buf.sem_flg = SEM_UNDO;
 if (semop (semid, &v_buf, 1) == -1){
   perror ("v (semid) failed");
   exit (1);
 }
 return (0);
}

 

p랑 v를 정해야함 semid는 하나밖에 없었었음 semid가 가리키는 0번 세마포어에다가 p를 지정하는 함수임
sembuf 가 필요함 그 오퍼레이션을 지정할 p_buf에는 작동하는 세마포어가 0 하나라서 num 은 0이고
op은 wait니까 음수 -1을 집어넣음 0번 세마포어에 대해서 -1 wait를 실행하고 옵션은 sem_undo
실제 수행하는건 if 문 안에 semop 임 그게 p_buf에 저장되어 있음 근데 p_buf[0]으로 되어야 함

 

#inculde "pv.h"

void handlesem (key_t skey);

main(){ /* testsem -- 세마포 루틴을 테스트한다. */
 key_t semkey = 0x200;
 int i;

 for(i = 0; i < 3; i++) if (fork() == 0) handlesem (semkey);
}

void handlesem (key_t skey){
 int semid;
 pid_t pid = getpid();
 if ((semid = initsem(skey)) < 0) exit (1);
 printf( "\nprocess %d before critical section\n", pid);
 p (semid);
 printf ("process %d in critical section\n", pid);

 /* 실제로는 무언가 흥미있는 일을 한다. */
 sleep (10);
 printf ("process %d leaving critical section\n", pid);
 v (semid);
 printf ("process %d exiting\n", pid);
 exit (0);
}

 

main에서 특정 번호를 주고 key를 200으로 주고 세번을 for문 주는데 fork를 함

parent는 무조건 false돼서 loop 세 번, parent는 fork 를 세 번함 child 세 개 만들고 끝남
child는 true가 되니까 handlesem을 들어와서 수행함 

 

끝날때 exit하니까 그걸 다시 loop로 돌아오진 않음 

child 가 두번째 세번째 실행할때 이미 key가 만들어져있어서 그냥 get만 함 

그리고 각각 세개의 child가 누가 먼저 p를 실행하냐에 따라서 - child가 셋이서 경쟁함 -

세개가 동시에 실행되다가 어느하나가 p를 실행하면 세마포어의 값을 1 감소시키고 내려옴 

print하고 10초 간격으로 p를 두번째 실행한 child 라는 뜻임

 

process 799 before critical section
process 799 in critical section
process 800 before critical section
process 801 before critical section
process 799 leaving critical section
process 801 in critical section
process 799 exiting
process 801 leaving critical section
process 801 exiting
process 800 in critical section
process 800 leaving critical section
process 800 exiting

 

The semop(2) : SEM_UNDO

여전히 시스템에 살아있음 그래서 child1이 p(s)하고 임계영역 실행하고 v하고 나감 p2는 p(s)에서 멈춰있음
undo를 해주는거는 v를 자동으로 실행해서 sem을 자동으로 복구시키는 거임 

그래서 프로세스가 확보한 리소스를 다 release해주는 작업을 해달라는 옵션을 제공하는데, 

실제로 복구해주는 거는 kernel이 해줌 프로세스는 이미 죽었기 때문임

 

wait를 하면 세마포어를 할당했다는 거임 그 때 세마포어를 undo flag를 지정하면 kernel이 기억하고
얼마나 많은 allocate했는지 커널이 기억함

그리고 프로세스가 종료하고 release안하고 종료할 때 자발적으로 terminate는 normal인데 

abnormal 하게 involuntarily하게 종료하면 그 세마포어에 대해서
감소한 만큼 리소스를 확보했단 말인데 그런게 있으면 되돌려놓는 거임 커널이


Shared Memory

 

shared memory는 프로세스는 원래 서로 다른 프로세스 간에는 변수를 공유하지 못함 
parent가 int x하고 fork 해도 child가 가진 int x와 parent int x는 같은 코드를 실행한다고 하더라도 같은 변수 아님
서로 다른 변수를 갖고있음 애초에 프로세스는 각각의 메모리를 할당받아 있음
그 메모리를 공유할 수 있게 해주면 클라이언트서버 간 데이터를 copy할 필요없어서 빠르게 메모리 공유할 수 있음
synchronize 문제가 생기게 됨 서버와 클라이언트가 메모리를 공유한다고 하면 

 

struct shmid_ds {
  struct ipc_perm shm_perm; /* see Section 15.6.2 */ 
  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 */ 
. . . 
};

 

쉐어드 메모리도 마찬가지로 id_ds가 있음
여러 개의 프로세스가 메모리를 공유하는 공간이 있음 그리고 첫번째는 perm이 들어감 
다섯번째는 mode가 들어가고 owner의 euid, guid가 있음 세번째는 creator의 euid guid 가 들어감
이 메모리의 사이즈, 그리고 op을 가장 최근에 실행한 프로세스, 그리고 그 creator 프로세스가 누구인지
여기에 creator는 uid가 있는거고 따로 있는 pid가 있음 


현재 p1 p2 p3가 shm이 attach되는데  

attach 된 프로세스가 몇 개인지 그리고 맨 마지막 attach 된 시간이 언젠지
떼어내는 것이 detach 하면 쉐어드 메모리 access 못함
그럼 shm가 변경된 시간이 언젠지 이 정보를 가져오려면 shmctl로 가져올 수 있음

 

The shmget(2) system call

#include <sys/shm.h>
int shmget(key_t key, size_t size, int flag);
// returns : shared memory ID if ok, -1 on error

 

shmget도 마찬가지로 key값을 넣어주면 id가 return 됨 shm의 size 그리고 flag는 별로 쓸 일 없음

 

 

The shmat(2) system call

#include <sys/shm.h>
void *shmat(int shmid, const void *addr, int flag);
// returns : pointer to shared memory segment if ok, -1 on error

 

shmget해서 id를 at에 넣으면 shm 첫 주소가 return 됨  first address
이 리턴값을 포인터로 shared memroy access가능

 

 

shared memory는 어떤 메모리 특정 영역인데

각 프로세스마다 주소 영역은 다르게 잡힐 수 있음 virtual memory임
physical address는 하나임

 

The shmdt(2) system call

#include <sys/shm.h>
int shmdt(void *addr);
// returns : 0 if ok, -1 on error

 

shmdt는 shmat에서 return 된 첫번째 주소 값을 넣어주면 이 주소가 가리키는 쉐어드 메모리를 프로세스에서 떼어냄

 

The shmctl(2) system call

#include <sys/shm.h>
int shmctl(int shmid, int cmd, struct shmid_ds *buf);
// returns : 0 if ok, -1 on error

 

shmctl보면 내용이 또 비슷함 
cmd에 들어가는 명령어가 아래에 있음 위에 세개는 동일함 stat는 정보를 가져오는 거임 
shm_id에 가져오는 거임 set은 그 값을 바꾸고 save하는 거 그 ipc 오브젝트를 지우는게 rmid이고
shm lock 메모리에 lock 해서 디스크 swapping 할때 swap out 안되게 fix 

 

 

#include <stdio.h>
#include <signal.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>

#define SHMKEY1   (key_t) 0x10   /* 공유 메모리 키 */
#define SHMKEY2  (key_t) 0x15   /* 공유 메모리 키 */
#define SEMKEY    (key_t) 0x20   /* 세마포 키 */

/* 읽기와 쓰기를 위한 버퍼의 크기 */
#define SIZ 5*BUFSIZ

/* 데이터와 읽은 계수를 저장한다.  */
struct databuf {
 int d_nread;
 char d_buf[SIZ];
};

typedef union _semun {
  int val; 
  struct semid_ds *buf;
  ushort *array;
} semun;

 

$ copy<infile>outfile 

read(0,buf) write(1,buf)해주면 됨
read하는 동안 write 못하고 write하는 동안 read 못함 그래서 이런 문제가 있어서
infile을 read하는 프로그램 buf를 두개 둠 buf1 buf2 두개를 두고 read(0,buf1) read(0,buf2)하고
write(1,buf1) write(1,buf2)함 디스크에 infile outfile이 있음
여기서 read buf1으로 infile 가져오는 동안 write 못함 물론 다 가져오면 buf 2로 가져오는 동안에는 write 1이 작동 가능

 

#include "share_ex.h"

#define IFLAGS (IPC_CREAT |IPC_EXCL)
#define ERR   ((struct databuf *)-1)

static int shmid1, shmid2, semid;

void getseg(struct databuf **p1, struct databuf **p2){
 /* 공유 메모리 영역을 생성한다 */
 if((shmid1=shmget(SHMKEY1,sizeof(struct databuf),0600|IFLAGS))==-1)fatal("shmget");
 if((shmid2=shmget(SHMKEY2,sizeof(struct databuf),0600|IFLAGS))==-1)fatal("shmget");
 /* 공유 메모리 영역을 부착한다. */
 if ((*p1 = (struct databuf *) shmat (shmid1, 0, 0)) == ERR) fatal ("shmat");
 if ((*p2 = (struct databuf *) shmat (shmid2, 0, 0)) == ERR) fatal ("shmat");
}

int getsem (void){  /* 세마포 집합을 얻는다 */
 semun x; x.val = 0;

 /* 두 개의 세마포를 갖는 집합을 생성한다 */
 if ((semid = semget (SEMKEY, 2, 0600|IFLAGS)) == -1) fatal ("semget");

 /* 초기값을 지정한다. */
 if (semctl (semid, 0, SETVAL, x) == -1) fatal ("semctl");
 if (semctl (semid, 1, SETVAL, x) == -1) fatal ("semctl");

 return (semid);
}

void remobj (void) {/* 공유 메모리 식별자와 세마포 집합 식별자를 제거한다. */
 if (shmctl (shmid1, IPC_RMID, NULL) == -1) fatal ("shmctl");
 if (shmctl (shmid2, IPC_RMID, NULL) == -1) fatal ("shmctl");
 if (semctl (semid, IPC_RMID, NULL) == -1)  fatal ("semctl");
}

 

쉐어드 메모리 두개를 준비함 하나는 buf1 하나는 buf2 세마포어는 두개가 있어야함 
버프사이즈는 기본 버프사이즈의 5배임 세마포어 컨트롤을 위해서 유니온이필요함 
getseg에 들어가는 데이터 스트럭처 ** 

 

/* reader -- 화일 읽기를 처리한다. */
#include "share_ex.h"

/* 이들은 두 세마포를 위해 p()와 v()를 정의한다. */
struct sembuf p1 = {0, -1, 0}, p2 = {1, -1, 0};
struct sembuf v1 = {0, 1, 0}, v2 = {1, 1, 0};

void reader (int semid, struct databuf *buf1, struct databuf *buf2){
   for(;;){
       /* 버퍼 buf1으로 읽어들인다. */
       buf1->d_nread = read(0, buf1->d_buf, SIZ);
       /* 동기화 지점 */
       semop (semid, &v1, 1);
       semop (semid, &p2, 1);

       /* writer가 수면하는 것을 피하기 위해 검사한다. */
       if (buf1->d_nread <=0) return;
       buf2->d_nread = read(0, buf2->d_buf, SIZ);
       semop(semid, &v1, 1);
       semop(semid, &p2, 1);
       if(buf2->d_nread <=0) return;
   }
}

 

#include "share_ex.h"

extern struct sembuf p1, p2; /* reader.c에 정의되어 있음. */
extern struct sembuf v1, v2; /* reader.c에 정의되어 있음. */

void writer (int semid, struct databuf *buf1,truct databuf *buf2){
   for(;;){
 	semop (semid, &p1, 1);
 	semop (semid, &v2, 1);

 	if (buf1->d_nread <= 0)
 		return;

 	write (1, buf1->d_buf, buf1->d_nread);

 	semop (semid, &p1, 1);
 	semop (semid, &v2, 1);

 	if (buf2->d_nread <= 0)
 		return;

 	write (1, buf2->d_buf, buf2->d_nread);
   }
}

 

메인 루틴을 보면 fork 해서 child 를 만듦 child는 write프로세스고 parent가 read임
buf의 포인터를 넘겨줌 각각 넘겨줌 그리고 getsem하면 세마포어를 가져옴 getseg해서 bufpointer를 가져옴
buf가 있으면 buf1이랑 buf2가 있는데 그런 변수가 명령은 $copy<infile>outfile 이렇게 들어감

 

#include "share_ex.h"

main(){
   int semid;
   pid_t pid;
   struct databuf *buf1, *buf2;

   /* 세마포 집합을 초기화한다. */
   semid = getsem();

   /* 공유 메모리 영역을 생성하고 부착한다. */
   getseg (&buf1, &buf2);
   switch (pid = fork()){
   case -1:
     fatal("fork");
   case 0: /* 자식 */
     writer (semid, buf1, buf2);
     remobj ();
   	 break;
   default: /* 부모 */
     reader (semid, buf1, buf2);
     break;
   }
   exit(0);
}

 

read2랑 write1이랑 parallel하게 실행 가능 둘 중 뭔가 먼저 끝나든 뭐 기다린다는 거같음
read2가 먼저 끝나면 read1이 write1이 끝날걸 기다려야함 그걸 조절해주는 게 p2 v2 애들임

 

'CS > 유닉스프로그래밍' 카테고리의 다른 글

유닉스  (0) 2020.12.11
14. 유닉스 - 소켓  (0) 2020.12.10
12. 유닉스 IPC - message queue, semaphore  (0) 2020.12.08
11. 유닉스 IPC - IO multiplexing  (0) 2020.12.08
10. 유닉스 IPC - PIPE, FIFO  (0) 2020.12.08