본문 바로가기

[pintOS] dup2 구현기

@정소민fan2025. 5. 24. 22:08

모든 과제를 PASS하고 시간이 많이 남아서 dup2 과제를 구현하기로 했다

 

dup2는 대체 무엇을 하는 함수일까?

dup2가 뭐지?

이미 열려있는 파일에 대한 포인터를 새로운 파일 디스크립터에 가져온다. 이러면 기존에 있던 파일에 대한 포인터가 새로 하나 생겨서 새 포인터로도 해당 파일에 접근이 가능해지는 것이다.

그럼 예시를 한번 보자

echo "Hello, World !!"

위 명령어는 Hello, World를 write 시스템 콜을 사용해서 stdout에 출력하라는 명령이다

echo "Hello, World !!" > hello.txt

 

위 명령어는 " Hello, World !! " 를 hello.txt에 저장하는 명령이다. 그런데 내부적으로는 어떤 시스템 콜을 사용할까? 원래 stdout에 출력하는 명령어였는데 > 하나 썼다고 hello.txt에 저장되는 이유가 뭘까?

 

실제 내부 동작은 어떻게 될까?

  1. open("hello.txt")를 통해 hello.txt 파일을 열고 파일 디스크립터를 얻는다
  2. dup2(fd, 1) 를 통해 방금 얻은 파일 디스크립터를 1(stdout) 자리에 덮어쓴다. 그럼 이제 hello.txt를 가리키는 파일 디스크립터가 fd와 1 두개가 되는 것이다
  3. 이제 write 시스템 콜을 사용해서 fd 1번(stdout)에 문자열을 출력한다
  4. 하지만 fd 1번은 dup2를 통해 hello.txt를 가리키고 있으므로 hello.txt에 문자열이 출력되어 저장  !!

너무 쉽다 !! 이제 dup2 과제 설명서를 보자

dup2 과제 설명서

과제 설명서

stdin, stdout을 닫을 수 있도록 바꿔야 한다고 한다. 그럼 file_close를 사용하면 될까? 절 대 안 된 다 ! !

왜냐? stdin, stdout은 파일이 아니라 콘솔과 연결된 fd이다. 따라서 file_close를 쓰면 예상치 못한 결과가 반환될 수 있다.

따라서 표준 입출력 닫혀있는지 알 수 있는 다른 필드를 만들어줘야 한다. 그리고 표준 입출력들도 파일 취급이 되니 dup2로 복제가 가능하다는 점도 알고 들어가자

 

그리고 위에서 dup2가 무엇인지 알아봤는데, 파일을 가리키는 포인터를 복제한다고 했다. 하나의 파일을 가리키는 fd 중 하나를 닫으면 다른 fd들도 다 닫아야할까? 그건 우리가 원하는 동작이 아니다. 다른 fd들이 여전히 해당 파일을 가리키고 있다면, 그 파일을 닫아서는 안된다 !! 따라서 몇 개의 fd들이 해당 파일을 가리키고 있는지 확인하는 필드도 필요하다.

 

이제 과제 설명서의 동작 규칙에 유의하며 로직을 구현해보자

thread.h 수정

struct thread
{
    ...
    int stdin_count;
    int stdout_count;
    ...
}

 

위에서 말했듯이 표준 입출력이 복사될수도 있고, 닫힐수도 있다. 따라서 표준 입출력이 모두 닫혀있는지 아닌지 확인할 카운트를 위해 필드 두개를 추가해줬다. 그럼 stderr은? pintos에서는 구현 안해도 된다

위 카운트가 0이라면 표준 입출력이 전부 닫혔다는 뜻이니까 그 점을 생각하면서 로직을 구현해주자

그리고 표준 입출력은 파일이 아니라고 했다. 그럼 어떻게 해당 fd가 복제되어있는지 구별해줄 것인가?

/* Project2 - extra */
#define STDIN 1
#define STDOUT 2

이를 구별하기 위해 해당 fd에 1과 2를 넣어줄 것이다. 당연히 이들을 dup2로 복제할 때는 file_duplicate를 사용하면 안된다 !!

주의할 점으로는 0을 넣어주면 안된다는 것이다

fd 테이블을 검사할 때 해당 fd가 비어있는지 확인하려면 NULL인지 아닌지 체크하게 되는데, 이때 해당 fd에 0이 들어가 있으면 

NULL로 간주해버리는 불상사가 생긴다.

struct file 수정

struct file{
    ...
    int dup_count;		 /* extra2 */
    ...
}

이 파일이 얼마나 복제되었는지 알기 위해 dup_count를 추가해준다. 이 dup 카운트가 0이라면 그때 비로소 file_close를 해 주면 될 것이다.

sys_dup2(int, int) 구현

int sys_dup2(int oldfd, int newfd)
{
	struct thread *cur = thread_current();

	/* oldfd가 유효하지 않으면, 실패하며 -1을 반환하고, newfd는 닫히지 않습니다. */
	if (oldfd < 0 || oldfd >= MAX_FD)
		return -1;

	if (cur->fd_table[oldfd] == NULL)
		return -1;

	/* oldfd와 newfd가 같으면, 아무 동작도 하지 않고 newfd를 반환합니다. */
	if (oldfd == newfd)
		return newfd;

	if (cur->fd_table[oldfd] == STDIN)
		cur->stdin_count++;
	else if (cur->fd_table[oldfd] == STDOUT)
		cur->stdout_count++;
	else
		increase_dup_count(cur->fd_table[oldfd]);

	/* newfd가 이미 열려 있는 경우, 조용히 닫은 후에 oldfd를 복제합니다. */
	if (cur->fd_table[newfd] != NULL)
	{
		lock_acquire(&filesys_lock);
		sys_close(newfd);
		lock_release(&filesys_lock);
	}
	cur->fd_table[newfd] = cur->fd_table[oldfd];

	return newfd;
}

그러면 어떻게 로직을 구현했는지 따라가보자

  1. dup2 테스트에는 유효하지 않은 fd를 복사하려는 시도가 있다. 이를 막기 위해 oldfd의 유효성을 검증해야한다
  2. oldfd와 newfd가 같다면 그냥 newfd를 리턴해주나다
  3. 만약 복제하려는 fd가 표준 입출력이라면 아까 추가해준 표준 입출력 카운트를 증가시켜준다
  4. fd가 표준 입출력이 아닌 파일이라면, 해당 파일 구조체 내의 dup_count를 증가시켜준다
  5. newfd가 이미 열려 있다면, 해당 fd를 비워준다. 이 때 file_close를 호출해서는 안되는데, 다른 fd들이 해당 파일을 가리키고 있을 수도 있기 때문이다
  6. 마지막으로 newfd에 oldfd를 복제해준다

뭐든 과제 설명서를 충실히 구현하면 문제는 없다 !!

/* filesys/file.c */
void increase_dup_count(struct file *file)
{
	file->dup_count++;
}

void decrease_dup_count(struct file *file)
{
	file->dup_count--;
}

int check_dup_count(struct file *file)
{
	return file->dup_count;
}

위 두 함수는 file.h에 file 구조체가 선언되어 있지 않아 구현한 함수이다.

이제 구현해준 sys_dup2를 syscall_handler에 등록해주자

void syscall_handler(struct intr_frame *f UNUSED)
{
    ...
    case SYS_DUP2:
        f->R.rax = sys_dup2(arg1, arg2);
        break;
    ...
}

이제 복제가 가능해졌으니 수정해줘야할 함수가 여럿 있다. 따라가보자

 

내 블로그에서는 시스템 콜 중 fork, exec, wait를 제외하고는 포스팅을 하지 않았다. 내가 담담한 부분이 아니기도 하고, 내부적으로 구현되어있는 함수를 적절히 사용해주고 예외처리만 제대로 해주면 될 듯 해서이다. 대신 팀원들의 포스팅을 링크에 걸어두도록 하겠다.

https://minhyay.tistory.com/157

 

User Program _System Call(exit, write, open, close, create, remove)

sys_exit이제 기본적인 시스템콜 구현을 보도록 하겠습니다유저 프로그램이 exit을 호출하면 내부적으로 다음과 같은 시스템 콜 래퍼 함수가 호출됩니다:void exit (int status) { syscall1(SYS_EXIT, status); //

minhyay.tistory.com

https://trash-in-trashcan.tistory.com/233

 

Pintos-Kaist 진행하기 (3): 사용자 프로그램(1)

1. 전체 구조 파악진행 순서는 다음과 같다.자식 프로세스 실행 제어 및 동기화자식 프로세스가 프로그램을 실행하는 동안, 부모 프로세스가 종료되지 않고 대기하도록 동기화 구현하기프로세

trash-in-trashcan.tistory.com

관련 함수 수정

sys_close(int)

void sys_close(int fd)
{
    struct thread *curr = thread_current();
    if (fd < 0 || fd >= MAX_FD)
        return;

    if (curr->fd_table[fd] == STDIN)
        curr->stdin_count--;

    if (curr->fd_table[fd] == STDOUT)
        curr->stdout_count--;

    struct file *file_object = curr->fd_table[fd];
    if (file_object == NULL || file_object == STDIN || file_object == STDOUT)
    {
        curr->fd_table[fd] = NULL;
        return;
    }
    decrease_dup_count(file_object);

    if (check_dup_count(file_object) == 0)
        file_close(file_object);
    curr->fd_table[fd] = NULL;
}

이제 close 시스템 콜은 신중해야 한다. 해당 fd가 표준 입출력인지, 아니라면 몇 개 복제되어있는지 알아야 하기 때문이다.

sys_close 로직은 크게 두가지로 나눠볼 수 있다.

1. 해당 fd가 표준 입출력일 때

2. 해당 fd가 파일일 때

표준 입출력이면 해당 fd를 비워주고 표준 입출력 카운트를 하나씩 낮춰주면 된다

그럼 실제 파일을 가리키는 fd라면? 해당 파일 구조체 내에 선언되어있는 dup_count를 1 낮춰주고, 카운트가 0이라면 실제로 해당 파일을 닫아주면 된다. 그 fd는 이제 NULL로 비워주면 된다

sys_read(int, void *, unsigned)

int sys_read(int fd, void *buffer, unsigned size)
{

    if (size == 0)
        return 0;

    check_buffer(buffer, size); // 페이지 단위 검사

    struct thread *cur = thread_current();

    if (fd < 0 || fd >= MAX_FD)
    {
        return -1;
    }

    // stdin 처리
    if (cur->fd_table[fd] == STDIN)
    {
        if (cur->stdin_count != 0)
        {
            for (unsigned i = 0; i < size; i++)
            {
                ((char *)buffer)[i] = input_getc();
            }
            return size;
        }
        return -1;
    }

    struct file *file_obj = cur->fd_table[fd];

    if (file_obj == NULL || file_obj == STDOUT)
    {
        return -1;
    }

    // 파일 읽기
    lock_acquire(&filesys_lock);
    int bytes_read = file_read(file_obj, buffer, size);
    lock_release(&filesys_lock);
    return bytes_read;
}

이제 read를 보자. close와 마찬가지로 해당 fd가 표준 입력인지, 아니면 파일인지 확인해야 한다.

해당 fd가 표준 입력이라면, stdin_count가 0인지 확인하고, 아니라면 콘솔에서 입력을 받으면 된다. 하지만 fd가 닫힐 때 stdin_count가 감소하는 로직만 제대로 작성했다면, 불필요한 확인일것 같기도 하다. 그렇지만 커널 레벨 코드는 뭐든 두번 세번 체크하는게 로직상 더 좋은것 같으니 해두자

그리고 해당 fd가 표준 출력일 수도 있으니, 그떄는 비정상적인 접근으로 간주하고 -1을 리턴해주자

sys_write(int, const void *, unsigned)

int sys_write(int fd, const void *buffer, unsigned size)
{
    check_buffer(buffer, size);
    // fd가 유효한지 먼저 검사
    if (fd < 0 || fd >= MAX_FD)
        return -1;

    struct thread *cur = thread_current();

    if (cur->fd_table[fd] == STDOUT && cur->stdout_count != 0)
    {
        putbuf(buffer, size);
        return size;
    }
    struct file *f = process_get_file(fd);
    if (f == NULL)
        return -1;

    lock_acquire(&filesys_lock);
    int bytes_written = file_write(f, buffer, size);
    lock_release(&filesys_lock);
    return bytes_written;
}

read와 마찬가지로 fd가 표준 출력을 가리키는지 확인하자. 그리고 fd가 파일이라면 file_write를 진행해주면 되겠다.

__do_fork(void *)

우리는 표준 입출력도 dup2로 복제가 가능해졌다. 그런데 fork시에는 file_duplicate로 fd를 복제해왔었다. 하지만 우리는 표준 입출력을 #define을 상수로 정의했었다. 이러한 상수를 file_duplicate로 복제해오면 당연히 커널 패닉이 뜰 것이다. 우리는 생각해두고 fd 테이블을 부모로부터 복사해오는 로직을 수정해야한다.

for (int fd = 0; fd < MAX_FD; fd++)
{
    if (fd <= 1)
        current->fd_table[fd] = parent->fd_table[fd];
    else
    {
        if (parent->fd_table[fd] != NULL)
            current->fd_table[fd] = file_duplicate(parent->fd_table[fd]);
    }
}

이것이 기존의 fd 테이블 복제 코드이다. 이 때의 표준 입출력들은 항상 fd 0,1에 있었지만 이제는 dup2를 통해 어느 번호에든 있을 수 있다. 따라서 이를 체크해서 복제해줘야 한다.

for (int fd = 0; fd < MAX_FD; fd++)
{
    if (fd <= 1)
        current->fd_table[fd] = parent->fd_table[fd];
    else
    {
        if (parent->fd_table[fd] != NULL)
        {
            if (parent->fd_table[fd] == STDIN || parent->fd_table[fd] == STDOUT)
                current->fd_table[fd] = parent->fd_table[fd];
            else
                current->fd_table[fd] = file_duplicate(parent->fd_table[fd]);
        }
    }
}

current->stdin_count = parent->stdin_count;
current->stdout_count = parent->stdout_count;

이제 해당 fd가 표준 입출력이라면, file_duplicate가 아닌 상수만 복제해오면 된다. 그리고 부모의 표준 입출력 카운트 또한 복제해오자.

process_exit(void)

if (curr->fd_table != NULL)
{
    for (int i = 0; i < MAX_FD; i++)
    {
        if (curr->fd_table[i] != NULL)
        {
            sys_close(i);
            curr->fd_table[i] = NULL;
        }
    }
}

이제 프로세스를 닫을 때도 해당 fd가 파일이 아닐수도 있으니 file_close를 사용하지 말고, sys_close를 사용해야 한다 !!

이렇게만 만들어주면?

이제 95개가 아닌 97개의 테스트가 통과되었다!!

pintos 별거없군

사실 빠져먹은 설명이 있을수도 있고 하니까.. 팀 레포를 링크해두도록 하겠다. 아무쪼록 보고 도움이 되었다면 스타 하나만 달아 다오...

https://github.com/Week09-11-Pinots/Pintos-user-program

 

GitHub - Week09-11-Pinots/Pintos-user-program: 10-11주차 과제

10-11주차 과제. Contribute to Week09-11-Pinots/Pintos-user-program development by creating an account on GitHub.

github.com

 

정소민fan
@정소민fan :: 코딩은 관성이야

코딩은 관성적으로 해야합니다 즐거운 코딩 되세요

목차