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

1. 전체 구조 파악


진행 순서는 다음과 같다.

  1. 자식 프로세스 실행 제어 및 동기화
    1. 자식 프로세스가 프로그램을 실행하는 동안, 부모 프로세스가 종료되지 않고 대기하도록 동기화 구현하기
  2. 프로세스 인자 파싱
  3. exit, write 구현 
  4. 그 외 시스템 콜 구현

 

이전에는 함수 전체를 모두 모아 하나로 포스팅했지만, 이번엔 이 진행 순서대로 코드를 올려보겠다.


2. 자식 프로세스 실행 제어 및 동기화


우선 시스템 콜을 구현하기 위해선 부모 프로세스를 대기시켜 놓을 필요가 있다.

이를 위해 process.c의 process_wait를 다음과 같이 구성해 두어야 한다.

int process_wait(tid_t child_tid UNUSED)
{

	timer_msleep(3000);

	return -1;
}

process_wait는 TID 프로세스가 종료되기를 기다리고 exit status를 반환한다. 만약 커널에 의해 종료되었다면(즉, 예외로 인해 kill 된 경우) -1을 반환한다. 

TID가 유효하지 않거나, 호출 프로세스의 자식이 아니거나, 이미 해당 TID에 대해 process_wait가 호출 된 적이 있다면 즉시 -1을 반환하고 기다리지 않는다.

다만 아직 여기까진 구현하지 않고, 단순히 대기하는 기능만 구현하였다. 

process_wait는 실행하는 컴퓨터의 사양에 맞춰 적절히 조절해야 한다. 


3. 프로세스 인자 파싱


인자를 파싱하고 함수에 인자를 전달하는 기능을 구현해야 한다. 

호출 규약의 주요 내용은 다음과 같다.

  • 함수 인자는 다음 순서로 레지스터에 전달됩니다: %rdi, %rsi, %rdx, %rcx, %r8, %r9
  • 호출자는 자신이 실행하던 다음 명령어 주소(복귀 주소)를 스택에 푸시한 뒤, 피호출 함수로 CALL 명령어를 통해 점프합니다. CALL 명령은 위 두 동작을 모두 수행합니다.
  • 피호출 함수는 실행을 시작합니다.
  • 반환값이 있다면, 피호출 함수는 %rax 레지스터에 결과를 저장합니다.
  • 피호출 함수는 RET 명령어로 스택에서 복귀 주소를 꺼내 그 주소로 점프하여 복귀합니다.

 

예를 들어, 다음과 같은 명령을 실행한다고 가정 해 보자.

/bin/ls -l foo bar

이 명령을 인자로 처리하는 절차는 다음과 같다:

  1. 단어로 분할: /bin/ls, l, foo, bar
  2. 스택 상단에 문자열 배치: 문자열을 스택의 최상단에 복사. 순서는 중요하지 않으며, 포인터로 참조될 예정이다.
  3. 각 문자열의 주소와 NULL 포인터를 푸시:
    • 오른쪽에서 왼쪽 순서로 각 argv[i]의 주소를 스택에 푸시
    • 마지막에 NULL 포인터 하나 추가 (argv[argc] = NULL)
    • 성능을 위해 첫 푸시 전에 스택 포인터를 8바이트 정렬 (word-aligned)
  4. 레지스터 설정:
    • %rdi = argc (인자 개수)
    • %rsi = argv 배열의 주소 (즉, argv[0]의 주소)
  5. 가짜 return address 푸시:
    • 실제로 _start()는 반환되지 않지만, 스택 프레임 형식 일관성을 위해 아무 주소나 푸시

 

0x4747ffb8 부터 시작하는 스택 포인터라고 해 보자. 표로 그리면 다음과 같다.

주소 이름
0x4747fffc argv[3][...] "bar\0"
0x4747fff8 argv[2][...] "foo\0"
0x4747fff5 argv[1][...] "-l\0"
0x4747ffed argv[0][...] "/bin/ls\0"
0x4747ffe8 word-align 0 (padding)
0x4747ffe0 argv[4] 0 (NULL)
0x4747ffd8 argv[3] 0x4747fffc
0x4747ffc0 argv[2] 0x4747fff8
0x4747ffc8 argv[1] 0x4747fff5
0x4747ffc0 argv[0] 0x4747ffed
0x4747ffb8 return addr 0 (dummy)

 

  • RDI = 4(argc)
  • RSI = 0x4747ffc0 (argv)

argc는 인자의 개수, RSI는 각각의 인자를 담고 있는 배열이다.

이렇게 명령어를 받으면 인자를 파싱하고 process_exec인자를 전달하도록 구현해야 한다. 인자 파싱은 pintos에서 제공해 주는 strtok_r() 함수를 사용할 것이다. 이 함수에 대한 자세한 설명은 lib/string.c에 주석으로 잘 되어 있다.

더보기

@ 코드 보기

process.c

static bool
load(const char *file_name, struct intr_frame *if_)
{
	//기존 내용..
    /* 스택을 설정합니다. */
	if (!setup_stack(if_))
		goto done;

	/* 시작 주소를 설정합니다. */
	if_->rip = ehdr.e_entry;

	for (int i = argc - 1; i >= 0; i--)
	{
		if_->rsp -= strlen(argv[i]) + 1;
		rsp_arr[i] = if_->rsp;
		memcpy((void *)if_->rsp, argv[i], strlen(argv[i]) + 1);
	}

	while (if_->rsp % 8 != 0)
	{
		if_->rsp--;				  // 주소값을 1 내리고
		*(uint8_t *)if_->rsp = 0; // 데이터에 0 삽입 => 8바이트 저장
	}

	if_->rsp -= 8; // NULL 문자열을 위한 주소 공간, 64비트니까 8바이트 확보
	memset(if_->rsp, 0, sizeof(char **));

	for (int i = argc - 1; i >= 0; i--)
	{
		if_->rsp -= 8; // 8바이트만큼 rsp감소
		memcpy(if_->rsp, &rsp_arr[i], sizeof(char **));
	}

	if_->rsp -= 8;
	memset(if_->rsp, 0, sizeof(void *));

	if_->R.rdi = argc;
	if_->R.rsi = if_->rsp + 8;

	success = true;

done:
	/* load의 성공 여부와 상관없이 여기로 도달합니다. */
	file_close(file);
	return success;
}
/* 현재 실행 컨텍스트를 f_name으로 전환합니다.
 * 실패 시 -1을 반환합니다. */
int process_exec(void *f_name)
{
	char *file_name = f_name;
	char cp_file_name[MAX_BUF];
	memcpy(cp_file_name, file_name, strlen(file_name) + 1);
	bool success;

	/* intr_frame을 thread 구조체 안의 것을 사용할 수 없습니다.
	 * 이는 현재 스레드가 재스케줄될 때,
	 * 그 실행 정보를 해당 멤버에 저장하기 때문입니다. */
	struct intr_frame _if;
	_if.ds = _if.es = _if.ss = SEL_UDSEG;
	_if.cs = SEL_UCSEG;
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* 현재 컨텍스트를 제거합니다. */
	process_cleanup();

	/* 그리고 이진 파일을 로드합니다. */
	ASSERT(cp_file_name != NULL);
	success = load(cp_file_name, &_if);

	/* 로드 실패 시 종료합니다. */
	palloc_free_page(file_name);
	if (!success)
		return -1;

	// hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true);
	/* 프로세스를 전환합니다. */
	do_iret(&_if);
	NOT_REACHED();
}

삼중 포인터로 구현한 조들이 많았는데, 삼중 포인터가 아니어도 구현 가능하다.

처음 파싱을 할 때 rip(스택포인터)는 ehdr.e_entry;로 설정한다.

또한, 인자 파싱은 load 내에서 해야 한다. load에서 스택을 설정하기 때문에, load 이전 process_exec에서 하게 되면 쓰레기 값에 접근하여 커널 패닉이 발생할 수 있다.


3. exit, write 구현


이번 과제에서 사용할 syscall.c 파일은 두 개가 있다. 

하나는 /lib/user/syscall.c 이고, 다른 하나는 /userprog/syscall.c이다. 우리가 작업할 syscall.c는 후자이며 전자는 수정할 필요가 없다. 다만 구현할 때, 어떤 인자를 받아야 하는지 참고할 때 전자를 사용하면 된다. 

syscall_handler를 수정해 주어야 하는데, 여기서 인자로 넘겨받는 f의 rax값을 기준으로 각기 다른 함수가 실행되어야 한다.

더보기

@코드 보기

/* The main system call interface */
void syscall_handler(struct intr_frame *f UNUSED)
{
	uint64_t syscall_num = f->R.rax;
	uint64_t arg1 = f->R.rdi;
	uint64_t arg2 = f->R.rsi;
	uint64_t arg3 = f->R.rdx;
	uint64_t arg4 = f->R.r10;
	uint64_t arg5 = f->R.r8;
	uint64_t arg6 = f->R.r9;

	switch (syscall_num)
	{
	case SYS_HALT:
		
		break;
	case SYS_EXIT:
		
		break;
	case SYS_FORK:
		
		break;
	case SYS_EXEC:
    
		break;
	case SYS_WAIT:
    
		break;
	case SYS_CREATE:
		
		break;
	case SYS_REMOVE:
	
		break;
	case SYS_OPEN:
		
		break;
	case SYS_FILESIZE:
		
		break;
	case SYS_READ:
		
		break;
	case SYS_WRITE:
	
		break;
	case SYS_SEEK:
    
		break;
	case SYS_TELL:
    
		break;
	case SYS_CLOSE:
    
		break;
	default:
		thread_exit();
		break;
    }
}

3.1. exit()


exit의 원형은 아래와 같다.

void
exit (int status) {
	syscall1 (SYS_EXIT, status);
	NOT_REACHED ();
}

exit는 현재 프로세스를 종료하고, 종료 코드를 커널에 전달한다. 이후의 코드는 실행되지 않는다.(NOT_REACHED() 사용)

 

간단하게, [스레드 이름: exit(상태)]의 출력을 하나 하고, 스레드를 종료시키면 된다.

switch (syscall_num)
	{
	case SYS_HALT:
		sys_halt();
		break;
	case SYS_EXIT:
		sys_exit(arg1);
		break;
    }

static void sys_exit(int status)
{
	struct thread *cur = thread_current();
	cur->exit_status = status;

	printf("%s: exit(%d)\n", thread_name(), status);
	thread_exit();
}

 

에러) 종료가 안돼요!

만약 exit를 구현했는데도 종료가 되지 않고 TIMEOUT이 발생한다면, process_wait를 수정할 필요가 있다.

만약 process_wait를 while(1){ } 과 같은 무한루프를 걸어버린다면 종료를 시킬 부모 프로세스가 없으므로 종료가 되지 않는다. 비슷하게, 만약 for문을 너무 많이 걸어버린다면 (컴퓨터 마다 사양이 다름) 60초 이내에 아무것도 출력하지 못할 수 있다.


3.2. write


wirte의 원형은 아래와 같다.

int
write (int fd, const void *buffer, unsigned size) {
	return syscall3 (SYS_WRITE, fd, buffer, size);
}

write는 열린 파일 디스크립터에 데이터를 쓴다.

  • fd: 쓸 대상 파일 디스크립터
  • buffer: 쓸 데이터가 담긴 메모리 주소
  • size: 쓸 바이트 수

 

static int sys_write(int fd, const void *buffer, unsigned size)
{
	
	if (fd == 1)
	{
		putbuf(buffer, size);
		return size;
	}

}

 

wirte는 인자로 넘겨받은 fd가 1이면 콘솔에 출력한다. 콘솔 출력은 putbuf로 가능하다. 

 

에러) 아무것도 출력되지 않아요! & 출력이 되다가 끝나버려요!

~~ didn't produce any output

write를 구현했음에도 와 같이 아무것도 출력되지 않는 경우 process_wait가 너무 짧아서일 수 있다. process_wait를 늘리면 해결 될 수도.


4. 그 외 시스템 콜 구현


4.1. sys_create()


create 의 원형은 다음과 같다.

bool
create (const char *file, unsigned initial_size) {
	return syscall2 (SYS_CREATE, file, initial_size);
}

create는 파일 시스템에 새 파일을 생성하는 시스템 콜을 호출한다. 

  • file: 생성할 파일의 이름(문자열 포인터)
  • initial_size: 파일의 초기 크기(바이트 단위)를 인자로 받는다.

 

더보기

@ 코드 보기

 

// 주소값이 유저 영역(0x8048000~0xc0000000)에서 사용하는 주소값인지 확인하는 함수
void check_address(const uint64_t *addr)
{
	struct thread *cur = thread_current();

	if (addr == "" || !(is_user_vaddr(addr)) || pml4_get_page(cur->pml4, addr) == NULL)
	{
		sys_exit(-1);
	}
}
bool sys_create(const char *file, unsigned initial_size)
{
	check_address(file);
	if (file == NULL || strcmp(file, "") == 0)
	{
		sys_exit(-1);
	}
	return filesys_create(file, initial_size);
}

 

file의 주소가 유저 영역 내부에 있는지 확인한 다음, filesys_create로 파일을 생성한다. filesys_create는 pintos에서 제공하는 만들어진 함수다.

 

더보기

@ filesys_Create

 

bool
filesys_create (const char *name, off_t initial_size) {
	disk_sector_t inode_sector = 0;
	struct dir *dir = dir_open_root ();
	bool success = (dir != NULL
			&& name !=NULL
			&& strlen(name)<=14
			&& free_map_allocate (1, &inode_sector)
			&& inode_create (inode_sector, initial_size)
			&& dir_add (dir, name, inode_sector));
	if (!success && inode_sector != 0)
		free_map_release (inode_sector, 1);
	dir_close (dir);

	return success;
}

filesy_create는 인자로 주어진 name과 initial_size로 파일을 생성하고, 성공하면 true, 실패하면 false를 반환한다. 

반환하는 값은 실제로 success인데, 인자로 주어지는 name이 NULL이 아니고, 14자 이하여아 하므로 그 부분은 수정하였다.


4.2. sys_remove


main에서 사용되는 remove 함수는 아래와 같다.

bool
remove (const char *file) {
	return syscall1 (SYS_REMOVE, file);
}

remove는 파일 시스템에서 주어진 파일을 삭제하는 시스템 콜을 호출한다.

  • file: 삭제할 파일 이름
더보기

@ sys_remove 코드 보기

bool sys_remove(const char *file)
{
	return filesys_remove(file);
}

딱히 해 줄건 없다.


4.3. sys_open


main에서 사용되는 open 함수는 다음과 같다.

int
open (const char *file) {
	return syscall1 (SYS_OPEN, file);
}

open은 주어진 이름의 파일을 여는 함수다. 성공하면 파일 디스크럽터를 반환한다. 실패시 -1을 반환할 수 있다.

  • file: 열고자 하는 파일 이름

 

sys_open에서는 파일 디스크럽터를 사용해야 한다. 각 스레드는 자신만의 파일 디스크럽터 테이블을 가지고 있으며, 이 테이블은 동적 할당으로 구현한다. 

우선 thread 구조체 부터 수정해야 한다.

struct thread
{
	/* Owned by thread.c. */
	tid_t tid;				   /* Thread identifier. */
	enum thread_status status; /* Thread state. */
	char name[16];			   /* Name (for debugging purposes). */
	int priority;			   /* 기부받은 우선순위 */
	int original_priority;	   /* 원래의 우선순위 */

	/* Shared between thread.c and synch.c. */
	struct list_elem elem; /* List element. */
	struct list donations; /* 자신한테 기부해준 리스트 */
	struct lock *pending_lock;

	int nice;			// 양보하려는 정도?
	fixed_t recent_cpu; // CPU를 얼마나 점유했나?
	struct list_elem all_elem;
	
	struct file **fd_table;		 // 파일 디스크럽터 테이블

	int exit_status;

파일 디스크럽터 테이블의 초기화는 process.c의 process_init에서 진행한다.

process_init(void)
{
	struct thread *current = thread_current();
	current->fd_table = calloc(MAX_FD, sizeof(struct file *));
	current->fork_sema = malloc(sizeof(struct semaphore));
	sema_init(current->fork_sema, 0);
	ASSERT(current->fd_table != NULL);
}

MAX_FD는 32로 설정해 주었다.

 

더보기

@ 코드 보기

 

int sys_open(const char *file)
{
	check_address(file);
	if (file == NULL || strcmp(file, "") == 0)
	{
		return -1;
	}
	struct file *file_obj = filesys_open(file);
	if (file_obj == NULL)
	{
		return -1;
	}
	int fd = find_unused_fd(file_obj);
	return fd;
}
int find_unused_fd(const char *file)
{
	struct thread *cur = thread_current();

	for (int i = 2; i <= MAX_FD; i++)
	{
		if (cur->fd_table[i] == NULL)
		{
			cur->fd_table[i] = file;
			return i;
		}
	}
}

find_unused_fd는 현재 스레드의 파일 디스크럽터 테이블을 검색하여,  사용하고 있지 않은 디스크럽터를 반환한다. 검색할때는 2 부터 해야한다. 

 

filesys_open은 인자로 주어진 name 의 파일을 연다. 파일을 여는데 성공하면 새로운 파일을 반환하고, 실패하면 NULL을 반환한다. name인 파일이 존재하지 않는 경우에, 또는 내부 메모리 할당에 실패할 경우 실패한다.

struct file *
filesys_open (const char *name) {
	struct dir *dir = dir_open_root ();
	struct inode *inode = NULL;
	bool exist=0;

	if (dir != NULL)
		exist=dir_lookup (dir, name, &inode);
	dir_close (dir);
	
	if(!exist){
		return NULL;
	}

	return file_open (inode);
}

t실제로 파일이 존재하는지 검색하는 함수는 dir_lookup 함수다. 

dir_lookup은 주어진 name으로 파일을 검색하고, 존재하면 true를, 실패하면 false를 반환한다. 성공시, INODE를 파일의 inode로 설정하고, 실패하면 null pointer로 설정한다. 호출자는 반드시 INODE를 받아야 한다.

bool
dir_lookup (const struct dir *dir, const char *name,
		struct inode **inode) {
	struct dir_entry e;

	ASSERT (dir != NULL);
	ASSERT (name != NULL);

	if (lookup (dir, name, &e, NULL)) //디렉터리에 name이라는 엔트리가 있는지 검색 
		*inode = inode_open (e.inode_sector); //있다 
	else
		*inode = NULL; //없다. 

	return *inode != NULL;
}

4.4. sys_close


main에서 사용하는 close 형태는 아래와 같다.

void
close (int fd) {
	syscall1 (SYS_CLOSE, fd);
}

close는 열린 파일 디스크럽터를 닫는다. 반환값은 없다. 커널 내부에서 자원 해제와 핸들 정리를 수행한다.

  • fd: 닫을 대상 파일 디스크럽터
더보기

@ 코드 보기

 

void sys_close(int fd)
{
	struct thread *curr = thread_current();
	if (fd < 2 || fd >= MAX_FD)
		return NULL;
	struct file *file_object = curr->fd_table[fd];
	if (file_object == NULL)
		return;

	file_close(file_object);
	curr->fd_table[fd] = NULL;

}

4.5. sys_write


main 에서 사용하는 함수 형태는 아래와 같다.

int
write (int fd, const void *buffer, unsigned size) {
	return syscall3 (SYS_WRITE, fd, buffer, size);
}

wirte는 열린 파일 디스크럽터에 데이터를 쓰는 함수다.

  • fd: 쓸 대상 파일 디스크럽터
  • buffer: 쓸 데이터가 담긴 메모리 주소
  • size: 쓸 바이트 수

반환 값은 실제로 쓴 바이트 수이며, 실패 시 -1을 반환할 수 있다.

더보기

@ 코드 보기

 

void check_buffer(const void *buffer, unsigned size)
{
	uint8_t *start = (uint8_t *)pg_round_down(buffer);
	uint8_t *end = (uint8_t *)pg_round_down(buffer + size - 1);
	struct thread *cur = thread_current();

	for (uint8_t *addr = start; addr <= end; addr += PGSIZE)
	{
		if (!is_user_vaddr(addr) || pml4_get_page(cur->pml4, addr) == NULL)
		{
			// printf("Invalid page address: %p\n", addr);
			sys_exit(-1);
		}
	}
}
struct file *
process_get_file(int fd)
{
	struct thread *cur = thread_current();

	if (fd < 2 || fd > MAX_FD)
		return NULL;

	return cur->fd_table[fd];
}
static int sys_write(int fd, const void *buffer, unsigned size)
{
	check_buffer(buffer, size);

	if (fd == 1)
	{
		putbuf(buffer, size);
		return size;
	}
	struct file *f = process_get_file(fd);
	if (f == NULL)
		return -1;

	int bytes_written = file_write(f, buffer, size);
	return bytes_written;
}

버퍼 크기가 유효한지 먼저 확인해야 한다. sys_write와 sys_read는 유저 프로그램이 넘긴 포인터(버퍼)를 사용하는데, 커널은 이 포인터가 정상적인 유저 공간 메모리를 가리키는지, 그리고 실제로 매핑된(접근 가능한) 페이지인지 반드시 확인해야 한다. 만약 확인하지 않고 접근하면 커널이 잘못된 주소(NULL이나 커널 영역)로 접근하게 되고, 커널 패닉, 보안 취약점, 시스템 크러시 등이 발생할 수 있다. 

이러는 이유는 버퍼도 결국 "페이지"단위로 관리되기 때문이다. 현대 운영체제는 모든 프로세스의 메모리(코드, 데이터, 스택, 버퍼 등)을 페이지 단위로 관리한다. 즉, 버퍼로 사용되는 메모리 역시 프로세스의 가상 주소 공간 내의 특정 페이지들에 해당한다. 버퍼가 임시 공간이든, 전역 변수든, 힙 할당이든 결국 페이지 테이블에 의해 관리되는 페이지다. 

fd가 1이면 콘솔 출력이다. 콘솔 출력은 putbuf를 사용한다.

fd가 1이 아니면 파일 디스크럽터 테이블을 이용해 해당하는 파일을 찾고 file_write로 작성한다.


4.6. sys_filesize


int
filesize (int fd) {
	return syscall1 (SYS_FILESIZE, fd);
}

filesize는 열린 파일의 크기를 반환하는 함수다.

  • fd: 파일 디스크럽터 (open을 통해 얻은 값)
더보기

@ 코드 보기

int sys_filesize(int fd)
{
	// 현재 스레드의 fd_table에서 해당 fd에 대응되는 file 구조체를 가져온다
	struct thread *cur = thread_current();

	// fd가 음수거나 MAX_FD 초과인 경우
	if (fd < 0 || fd >= MAX_FD)
	{
		return -1;
	}

	// 파일 객체 가져오기
	struct file *file_obj = cur->fd_table[fd];
	if (file_obj == NULL)
	{
		return -1;
	}

	off_t size = file_length(file_obj);
	return size;
}

 

오류) 제대로 구현했는데 계속 틀렸다고 나와요!

filesize를 테스트하는 sample.txt의 문제일 가능성이 있다. sample.txt는 tests/userprog아래에 있다. 여기에 찾아가서 dos2unix sample.txt를 한 다음 다시 시도해 보자.

 


4.7. sys_read


int
read (int fd, void *buffer, unsigned size) {
	return syscall3 (SYS_READ, fd, buffer, size);
}

read는 열린 파일 디스크립터로부터 데이터를 읽는 함수다. 

  • fd: 읽을 대상 파일 디스크립터
  • buffer: 데이터를 저장할 버퍼 (사용자 메모리 주소)
  • size: 읽을 바이트 수

반환값은 실제로 읽은 바이트 수이고, 실패 시 -1을 반환할 수 있다.

 

더보기

@ 코드 보기

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 (fd == 0)
	{
		for (unsigned i = 0; i < size; i++)
		{
			((char *)buffer)[i] = input_getc();
		}
		return size;
	}

	struct file *file_obj = cur->fd_table[fd];
	if (file_obj == NULL)
	{
		return -1;
	}

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

사용되는 함수 중 input_getc()가 재미있다.

uint8_t
input_getc (void) {
	enum intr_level old_level;
	uint8_t key;

	old_level = intr_disable ();
	key = intq_getc (&buffer);
	serial_notify ();
	intr_set_level (old_level);

	return key;
}
uint8_t
intq_getc (struct intq *q) {
	uint8_t byte;

	ASSERT (intr_get_level () == INTR_OFF);
	while (intq_empty (q)) {
		ASSERT (!intr_context ());
		lock_acquire (&q->lock);
		wait (q, &q->not_empty);
		lock_release (&q->lock);
	}

	byte = q->buf[q->tail];
	q->tail = next (q->tail);
	signal (q, &q->not_full);
	return byte;
}

타고 들어가면 intq_getc라는 함수가 보인다. 이 함수는 원형 큐 q 가 비어있다면 계속 wait로 기다린다. 그런데 그냥 기다리는게 아니라 lock을 받았다 내려놨다 하면서 기다린다. 그래서 이것만 보면 왜 이런 형태로 되어있는지 이해하기 힘들고, 다른 쌍인 intq_putc를 보아야 한다.

void
intq_putc (struct intq *q, uint8_t byte) {
	ASSERT (intr_get_level () == INTR_OFF);
	while (intq_full (q)) {
		ASSERT (!intr_context ());
		lock_acquire (&q->lock);
		wait (q, &q->not_full);
		lock_release (&q->lock);
	}

	q->buf[q->head] = byte;
	q->head = next (q->head);
	signal (q, &q->not_empty);
}

intq_putc는 반대로 원형 큐 q가 가득 차 있다면 wait를 도는 함수다. 만약 우리가 큐가 가득 차지 않았다면 intq_getc에서는 대기를 하고 있을 것이고 intq_putc는 내려가 q의 버퍼에 바이트를 쓰고, q가 비어있지 않다는 신호를 보낼 것이다. 이 신호를 보면 intq_getc에서 잠자고 있던 큐는 깨어나서 락을 내려놓고, 버퍼에 쓰여있던 바이트를 읽어들인 후 큐가 가득차지 않았다는 신호를 보낸다.

정리하자면 이렇다.

  1. intq_getc
    • 역할: 큐에서 데이터를 꺼내는 소비자
    • 동작
      • 큐가 비어 있으면(lock을 잡고) not_empty 조건 변수에서 wait(대기) 한다.
      • 다른 스레드가 데이터를 넣어서 not_empty를 signal하면 깨어난다.
      • 데이터를 꺼내고 not_full을 signal 해서 생산자에게 "공간이 생겼다"고 알린다.
  2. intq_putc
    • 역할: 큐에 데이터를 넣는 생산자.
    • 동작
      • 큐가 가득 차 있으면(lock을 잡고) not_full 조건 변수에서 wait(대기) 한다.
      • 다른 스레드가 데이터를 꺼내서 not_full을 signal하면 깨어난다.
      • 데이터를 넣고, not_empty를 signal 해서 소비자에게 "데이터가 생겼다"고 알린다.

 

off_t
file_read (struct file *file, void *buffer, off_t size) {
	off_t bytes_read = inode_read_at (file->inode, buffer, size, file->pos);
	file->pos += bytes_read;
	return bytes_read;
}

file_read는 file에서 size 만큼 바이트를 읽고 buffer에 넣는 함수다. 파일의 현재 위치에서 부터 읽는다. 실제로 읽은 바이트의 수를 리턴한다.


4.8. sys_seek


void
seek (int fd, unsigned position) {
	syscall2 (SYS_SEEK, fd, position);
}

seek은 파일 디스크립터의 읽기/쓰기 포인터를 지정한 위치로 이동시키는 함수다.

  • fd: 대상 파일 디스크립터
  • position : 이동할 바이트 위치(파일 시작 기준 오프셋)

반환값은 없으며, 위치 이동만 수행된다.

더보기

@ 코드 보기

void sys_seek(int fd, unsigned position)
{
	struct thread *cur = thread_current();

	/* 유효하지 않은 파일 디스크립터인 경우 아무 작업도 하지 않음 */
	if (fd < 0 || fd >= MAX_FD)
	{
		return;
	}

	/* fd 테이블에서 해당 파일 객체 가져오기 */
	struct file *file_obj = cur->fd_table[fd];

	/* 파일이 열려 있지 않다면 리턴 */
	if (file_obj == NULL)
	{
		return;
	}

	/* 파일의 현재 읽기/쓰기 위치를 position으로 이동 */
	file_seek(file_obj, position);
}
void
file_seek (struct file *file, off_t new_pos) {
	ASSERT (file != NULL);
	ASSERT (new_pos >= 0);
	file->pos = new_pos;
}

file_seek은 파일의 현재 위치를 새로운 new_pos로 이동시키는 함수다.


4.9. sys_tell


unsigned
tell (int fd) {
	return syscall1 (SYS_TELL, fd);
}

tell은 현재 파일 디스크립터의 읽기/쓰기 포인터 위치를 반환한다.

  • fd: 대상 파일 디스크립터

반환값은 현재 오프셋 위치(바이트 단위)이다.

더보기

@ 코드 보기

 

unsigned sys_tell(int fd)
{
	struct thread *cur = thread_current();

	/* 유효하지 않은 파일 디스크립터인 경우 -1 반환 (unsigned지만 오류 표시로 사용) */
	if (fd < 0 || fd >= MAX_FD)
	{
		return -1;
	}

	/* fd 테이블에서 해당 파일 객체 가져오기 */
	struct file *file_obj = cur->fd_table[fd];

	/* 파일이 열려 있지 않다면 -1 반환 */
	if (file_obj == NULL)
	{
		return -1;
	}

	/* 현재 파일의 커서 위치 반환 */
	return file_tell(file_obj);
}
off_t
file_tell (struct file *file) {
	ASSERT (file != NULL);
	return file->pos;
}

file_tell은 FILE의 시작 위치로부터 현재 위치를 바이트 오프셋으로 반환하는 함수다.