출처: https://3months.tistory.com/307 [Deep Play]

3-2/운영체제

Process API

코딩하는 랄뚜기 2021. 9. 9. 10:32

fork()

fork()를 해주면 child process는 부모와는 다른 프로세스에 메모리를 할당받게 된다. 하지만 부모와 contents는 같다. 새롭게 생성된 process는 이것만의 registers와 PC(program counter register)를 갖게 된다. parent 에서 fork()는 child process의 PID를 retun 해주고 child에서는 0을 return 한다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(int argc,char* argv[]){
    //getpid()는 실행 중인 process id를 구해준다
    printf("hello world (pid:%d)\n",(int) getpid());
    //child는 여기서 부터 실행
    int rc = fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }else if(rc == 0 ){
        printf("hello, I am child (pid:%d)\n",(int) getpid());
    }else{
        printf("hello, I am parent of %d (pid:%d)\n",rc,(int) getpid());
    }
    return 0;
}

위에 코드 실행 결과

위에 코드는 순서가 바뀔 수도 있다는 것을 명심하자!!

fork가 실행될 때 Process가 할당되는 구조


wait()

parent에서 wait()을 하게 되면 parent process는 child가 run하고 exit하기 전까지 return을 할 수 없다. parent와 child는 실제로는 dependency가 없지만 wait()를 이용하면 child가 끝나야 parent가 실행되게 코드를 짤 수 있다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc,char* argv[]){
    printf("hello world (pid:%d)\n",(int) getpid());
    int rc = fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }else if(rc == 0 ){
        printf("hello, I am child (pid:%d)\n",(int) getpid());
    }else{
        int wc=wait(NULL);
        //wc에는 child에 pid가 저장된다
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",rc,wc,(int) getpid());
    }
    return 0;
}

위 코드 실행 결과

위에 코드의 실행 결과이다. fork만한 것과 달리 항상 저 결과가 나오게 된다. 따라서 Deterministic하다고 할 수 있다.


exec()

fork를 하면 child는 parent와 같은 content를 같는데, 혹시 이를 원치 않는다면 exec()를 사용하여 다른 프로그램이 되게 할 수 있다. 두 가지 변수를 필요로 하는데 the name of the binary file, the array of arguements이다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main(int argc,char* argv[]){
    printf("hello world (pid:%d)\n",(int) getpid());
    int rc = fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }else if(rc == 0 ){
        printf("hello, I am child (pid:%d)\n",(int) getpid());
        char* myargs[3];
        myargs[0]=strdup("wc"); //program: "wc" (word count)
        myargs[1]=strdup("p3.c"); //argument: file to count
        myargs[2]=NULL; // marks end of array
        execvp(myargs[0],myargs); // runs word count
        printf("this sholdn't print out");
    }else{
        int wc=wait(NULL);
        printf("hello, I am parent of %d (wc:%d) (pid:%d)\n",rc,wc,(int) getpid());
    }
    return 0;
}

위를 실행 했을 때 결과

실행 결과를 보면 printf("this sholdn't print out")이 출력되지 않고 26 80 800 p3.c가 출력 된 것을 알 수 있다.

여기서 26, 80, 800은 각각 p3.c의 줄의 수, 단어의 수, 글자 수(공백포함)을 의미한다.

exec()를 사용했을 때 process 구조


그렇다면 왜 fork와 exec를 구분해 놓았을까? 그 이유는 IO redirection과 pipe를 가능하게 위해서이다.

%ls -l 이 실행된다고 했을 때 shell은 fork()를 부르고 exec("ls","ls-l")이 된다.

%wc w3.c > newfile.txt를 실행한다고 가정 했을 때, exec("wc","wc w3.c")를 실행하기 전에 STDOUT(close(1))을 하고 open newfile.txt를 하게 된다.

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

int main(int argc,char* argv[]){
    printf("hello world (pid:%d)\n",(int) getpid());
    int rc = fork();
    if(rc<0){
        fprintf(stderr,"fork failed\n");
        exit(1);
    }else if(rc == 0 ){
        close(STDOUT_FILENO);
        open("./p4.output",O_CREAT|O_WRONLY|O_TRUNC,S_IRWXU);
        //now exec "wc"....
        char* myargs[3];
        myargs[0]=strdup("wc");
        myargs[1]=strdup("p4.c");
        myargs[2]=NULL;
        execvp(myargs[0],myargs);
    }else{
        int wc=wait(NULL);
    }
    return 0;
}

위의 코드를 실행한 결과이다. 중요한 점은 close(STDOUT_FILENO)인데 이것을 안하면 아래와 같이 c를 실행하는데 wc의 값이 나오게 되고 p4.output에는 아무것도 들어가지 않게 된다.

close(STDOUT_FILENO)에 주석을 쳤을 경우


File descriptor

File descriptor의 0번째 index에는 STDIN이 1번째 index에는 STDOUT이 2번째 index에는 STDERR이 들어간다.

따라서 n=open("file")과 같은 명령어는 3번부터 할당 되게 된다.


File offset

offset은 현재 파일의 어디를 읽고 있는지를 저장하고 있다.

struct file in Linux

memory에 생성된 offset은 fileA에서 정보를 받아오고 있고 process의 pointer는 그 offset을 가리키고 있다.


file descriptor and system calls

1.open() - 완전히 새롭게 할당 시켜준다.

2.close() - descriptor를 deallocate시켜준다. 단, descriptor들이 다른 파일들과 연관되어 있으면 안된다.

3.fork() - parent의 file descriptor table을 복사하여 child process에 넘겨준다.

4.exec() - file descriptor table을 유지한다.

fork() and file description

위 코드를 실행하면 hello world가 나오게 된다.

읽고 싶다면 close(0)을 하고 읽고 싶은 파일을 넣는다.

 

쓰고 싶다면 close(1)을 하고 쓰고 싶은 파일을 넣는다.
cat input.txt는 위와 같이 코딩 되어있다.
네모친 곳을 잘 보자 개념이 중요하다


pipe: '|'

파이프의 구조

파이프는 parent의 데이터를 child에서 사용할 수 있게 해준다.

dup()

dup()는 filedescriptor table에서 비어있는 곳을 찾아준다.

pipe(p)를 하면 p[0]은 read를 의미하고 p[1]은 write를 의미한다.
Pipe는 구조

부모의 read는 자식의 write를 받아오고 자식의 read는 부모의 write을 받아 온다.

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>

#define BUFSIZE 30

int main(int argc, char* argv[]){
    int fd[2];
    char buffer[BUFSIZE];
    pid_t pid;
    int state;
    state = pipe(fd);
    if(state == -1){
        puts("pipe() error");
        exit(1);
    }
    pid = fork();
    if(pid==-1){
        puts("fork() error");
        exit(1);
    }else if(pid == 0){
        read(fd[0],buffer,BUFSIZE); //부모에게서 값을 받는다.
        puts(buffer);
        write(fd[1],"자식이 부모에게",25); //부모에게 값을 준다.
    }else{
        write(fd[1],"부모가 자식에게",25); //자식에게 값을 준다.
        wait(NULL); //자식 process가 끝날 때까지 기다려 준다.
        read(fd[0],buffer,BUFSIZE); //자식에게 서 온 값을 받는다.
        puts(buffer);
    }
    return 0;
}

파이프 구조를 보고 위 코드를 보면 코드가 어떻게 돌아가는지 이해가 된다. 

ordinary pipe코드의 한계는 한쌍의 processes만을 이어준다는 것이다. 이를 해결하기 위해 있는 것이 Named pipe이다.


Named Pipe

Named pipe는 bi-directional하고 부모-자식 관계가 없다.그리고 여러 writers를 둘 수 있다.

//reader.c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#define MAX_BUF 1024

int main(){
    int fd;
    char* myfifo="/tmp/myfifo";
    char buf[MAX_BUF];
    
    //open,read and display the message from the FIFO
    fd=open(myfifo,O_RDONLY);
    read(fd,buf,MAX_BUF);
    printf("Received : %s\n",buf);
    close(fd);
    return 0;
}
//writer.c
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
int main(){
    int fd;
    char* myfifo="/tmp/myfifo";
    
    //create the FIFO (named pipe)
    mkfifo(myfifo,0666);
    
    //write "HI" to th FIFO
    printf("open without O NONBLOCK\n");
    fd=open(myfifo,O_WRONLY);
    write(fd,"HI",sizeof("HI"));
    close(fd);
    
    //remove the FIFO
    unlink(myfifo);
    return 0;
}

writer을 실행하면 다른 프로그램이 받아 줄 때까지 기다린다. 다른 터미널에서 read를 실행해주면 "HI"를 받고 reader가 종료되고 그 후에 writer가 종료된다.


pipe vs IO redirection

pipe는 스스로 그들을 지운다. 하지만 temporary file을 사용하면 명시적으로 지워줘야한다.

pipe는 아무 데이터나 통과 할 수 있지만 file redirection은 충분한 디스크 공간이 필요하다.

pipe는 reader와 writer가 동시에 proceed 될 수 있지만 redirection은 다른 하나가 끝나야 다른 하나가 시작 할 수 있다.

'3-2 > 운영체제' 카테고리의 다른 글

Multiprocessor Scheduling  (0) 2021.09.24
Scheduling:The Multi-Level Feedback Queue  (0) 2021.09.17
Scheduling:Introduction  (0) 2021.09.17
Mechanism:Limited Direct Execution  (0) 2021.09.10
Process  (0) 2021.09.03