이 전 포스팅에서 파일 디스크립터에 대해 알아보고 pintos에서 파일 디스크립터를 어떻게 구현할지 알아보았다. 이제 파일과 관련된 시스템콜 중 lock을 이용해 파일에 대한 동시 접근을 제어하려고 한다.
2025.05.25 - [크래프톤 정글] - [pintos] Week2~3: User Program Part.5 - 파일 디스크립터
[pintos] Week2~3: User Program Part.5 - 파일 디스크립터
이전 포스팅들에서 exec, halt, exit, create, remove를 구현했다. 다음 단계로 넘어가기 위해 파일디스크립터가 무엇인지 어떻게 구현할지 알고 넘어가야 한다.파일디스크립터는 운영체제 전반에 걸쳐
www.gowoong.com
락이 필요한 이유
- 여러 스레드가 filesys_open()을 동시에 호출하면, 내부적으로 사용하는 자료구조(예: inode, open file list 등)가 깨질 수 있다.
- 커널 수준에서는 이런 공유 자원에 대해 보호 장치 없이 접근하면 Data Race(경쟁 조건)이 생긴다.
- suscall_init() 에서 lock 초기화
- userprog/syscall.h
헤더파일에 lock 선언
struct lock filesys_lock;
#endif /* userprog/syscall.h */
syscall_init() 수정
- userprog/syscall.c
void
syscall_init (void) {
write_msr(MSR_STAR, ((uint64_t)SEL_UCSEG - 0x10) << 48 |
((uint64_t)SEL_KCSEG) << 32);
write_msr(MSR_LSTAR, (uint64_t) syscall_entry);
/* The interrupt service rountine should not serve any interrupts
* until the syscall_entry swaps the userland stack to the kernel
* mode stack. Therefore, we masked the FLAG_FL. */
write_msr(MSR_SYSCALL_MASK,
FLAG_IF | FLAG_TF | FLAG_DF | FLAG_IOPL | FLAG_AC | FLAG_NT);
lock_init(&filesys_lock); // Lock 초기화
}
시스템 콜 구현
시스템 콜 선언
- include/userprog/syscall.h
int open(const char *file);
int filesize(int fd);
int read(int fd, void *buffer, unsigned length);
int write(int fd, const void *buffer, unsigned length);
void seek(int fd, unsigned position);
int tell(int fd);
void close(int fd);
open() 구현
- userprog/syscall.c
int open(const char *file) {
check_address(file);
lock_acquire(&filesys_lock);
struct file *newfile = filesys_open(file);
lock_release(&filesys_lock);
if (newfile == NULL)
return -1;
int fd = process_add_file(newfile);
if (fd == -1)
file_close(newfile);
return fd;
}
pintos에서는 open에 lock을 지정하지 않아도 통과하고 있다. 하지만 open에서 file을 다룰 때 lock을 사용하는 것이 좋다.
struct file *newfile = filesys_open(file);
- 실제 파일 시스템에서 file 이름을 가진 파일을 연다.
- 성공 시 struct file * 반환, 실패 시 NULL 반환
int fd = process_add_file(newfile);
- 열었던 파일 객체를 현재 프로세스의 파일 디스크립터 테이블(FDT)에 등록하고, FD 번호를 받아온다.
- process_add_file() 내부에서:
- curr->fdt[curr->fd_idx] = newfile;
- fd_idx 증가
f (fd == -1)
file_close(newfile);
- FDT가 꽉 차서 더 이상 FD를 할당할 수 없는 경우 (fd == -1)
- 이미 열린 newfile은 더 이상 참조할 곳이 없으므로 자원 누수 방지 차원에서 닫음
return fd;
}
filesize() 구현
- userprog/syscall.c
int filesize(int fd) {
struct file *file = process_get_file(fd);
if (file == NULL)
return -1;
return file_length(file);
}
- 파일의 크기를 알려주는 시스템 콜
- 성공 시 파일의 크기를 반환, 실패 시-1 반환
read() 구현
- userprog/syscall.c
int read(int fd, void *buffer, unsigned size) {
check_address(buffer);
if (fd == 0) { // 0(stdin) -> keyboard로 직접 입력
int i = 0; // 쓰레기 값 return 방지
char c;
unsigned char *buf = buffer;
for (; i < size; i++) {
c = input_getc();
*buf++ = c;
if (c == '\0')
break;
}
return i;
}
// 그 외의 경우
if (fd < 3) // stdout, stderr를 읽으려고 할 경우 & fd가 음수일 경우
return -1;
struct file *file = process_get_file(fd);
off_t bytes = -1;
if (file == NULL) // 파일이 비어있을 경우
return -1;
lock_acquire(&filesys_lock);
bytes = file_read(file, buffer, size);
lock_release(&filesys_lock);
return bytes;
}
주어진 fd(파일 디스크립터)로부터 size만큼의 데이터를 읽어 buffer에 저장하고, 읽은 바이트 수를 반환한다.
- file_read()는 읽은 바이트 수를 반환
- 읽은 양이 0 이상이면 성공, -1이면 실패
if (fd == 0) {
int i = 0;
char c;
unsigned char *buf = buffer;
for (; i < size; i++) {
c = input_getc(); // 키보드에서 문자 1개 읽기
*buf++ = c; // 버퍼에 저장
if (c == '\0') // 널 문자 오면 종료 (선택적 로직)
break;
}
return i;
}
- 0은 표준 입력(stdin)을 의미 → 키보드 입력을 처리
if (fd < 3)
return -1;
- fd == 1(stdout), fd == 2(stderr)은 쓰기 전용이라 읽을 수 없음
- fd < 0은 잘못된 값 → 에러 처리
struct file *file = process_get_file(fd);
off_t bytes = -1;
if (file == NULL)
return -1;
- 주어진 fd에 해당하는 열린 파일 구조체 포인터를 얻는다
- 유효하지 않은 FD일 경우 실패 처리
- off_t는 C 언어나 POSIX 시스템에서 파일 오프셋(offset) 또는 파일 크기를 표현할 때 사용하는 정수형 타입이다.
lock_acquire(&filesys_lock);
bytes = file_read(file, buffer, size);
lock_release(&filesys_lock);
return bytes;
write() 구현
- userprog/syscall.c
int write(int fd, const void *buffer, unsigned size) {
check_address(buffer);
off_t bytes = -1;
if (fd <= 0) // stdin에 쓰려고 할 경우 & fd 음수일 경우
return -1;
if (fd < 3) { // 1(stdout) * 2(stderr) -> console로 출력
putbuf(buffer, size);
return size;
}
struct file *file = process_get_file(fd);
if (file == NULL)
return -1;
lock_acquire(&filesys_lock);
bytes = file_write(file, buffer, size);
lock_release(&filesys_lock);
return bytes;
}
파일 디스크립터 fd가 가리키는 파일이나 장치(stdout 등)에 데이터를 기록하고, 성공한 바이트 수를 반환. 실패 시 -1.
off_t bytes = -1;
- file_write()의 반환값(성공한 바이트 수)을 저장할 변수
- 실패했을 경우를 대비해 초기값을 -1로 설정
- off_t는 바이트 수 표현에 사용하는 타입
if (fd <= 0)
return -1;
- fd == 0: stdin은 읽기 전용 → 쓰기 금지
- fd < 0: 잘못된 파일 디스크립터 → 에러
if (fd < 3) {
putbuf(buffer, size);
return size;
}
- stdout(1) 또는 stderr(2)인 경우 → 콘솔에 출력
- putbuf()는 Pintos 제공 함수로, 화면에 문자열 출력
- 성공한 바이트 수 = 요청한 바이트 수 그대로 리턴
struct file *file = process_get_file(fd);
if (file == NULL)
return -1;
- 유저의 파일 디스크립터를 내부 struct file *로 변환
- process_get_file(fd)에서 FDT 범위 초과나 NULL이면 실패
lock_acquire(&filesys_lock);
bytes = file_write(file, buffer, size);
lock_release(&filesys_lock);
return bytes;
- file_write()는 파일 시스템의 inode, 디렉터리, 버퍼 등을 수정
- 다중 스레드가 동시에 접근하면 Race Condition 발생
- 따라서 filesys_lock으로 전역 보호
seek() 구현
- userprog/syscall.c
void seek(int fd, unsigned position)
{
struct file *file = process_get_file(fd);
if (fd < 3 || file == NULL)
return;
file_seek(file, position);
}
유저가 연 파일에서 다음에 읽거나 쓸 위치(offset)를 변경한다.
file_seek(file, position)을 호출하여 파일의 커서 위치를 이동시킨다.
struct file *file = process_get_file(fd);
- 현재 스레드의 파일 디스크립터 테이블(FDT)에서 fd에 해당하는 struct file *을 가져옴
- 이 파일 포인터가 NULL이면 아직 열린 파일이 아니거나 잘못된 FD라는 뜻
if (fd < 3 || file == NULL)
return;
- fd < 3일 경우 (stdin=0, stdout=1, stderr=2):
→ 이들은 seek이 의미 없는 장치이므로 무시 - file == NULL:
→ 유효하지 않은 FD거나 닫힌 파일 → 무시 - 따라서, seek이 유효한 경우에만 아래를 수행
file_seek(file, position);
- 파일의 현재 offset(커서 위치)를 position으로 이동
- 이후 read()나 write()는 이 위치부터 수행됨
이 함수가 필요한 이유
- 읽거나 쓸 때 파일의 현재 위치를 변경할 수 있어야 유연한 입출력이 가능함
- 예:
- seek(fd, 100); read(fd, buf, 20); → 100번 바이트부터 20바이트 읽음
- seek(fd, 0); → 파일 처음으로 이동
tell() 구현
- userprog/syscall.c
int tell(int fd)
{
struct file *file = process_get_file(fd);
if (fd < 3 || file == NULL)
return -1;
return file_tell(file);
}
유저가 연 파일 디스크립터 fd에 대해, 현재 커서(읽기/쓰기 위치)가 어디인지 반환한다.
struct file *file = process_get_file(fd);
- 현재 스레드의 파일 디스크립터 테이블(FDT)에서 fd에 해당하는 파일 구조체를 가져옴
- 유효하지 않은 fd이거나 닫힌 파일일 경우 file == NULL이 됨
if (fd < 3 || file == NULL)
return -1;
- fd < 3: stdin(0), stdout(1), stderr(2)은 seekable 한 파일이 아니므로 무시
- file == NULL: 해당 FD가 열려 있지 않음 → 실패
- 실패한 경우 -1을 반환
return file_tell(file);
- Pintos 내부의 file_tell() 함수를 호출하여 파일의 현재 offset 값을 반환
- 보통 file->pos 값을 그대로 반환하는 함수
close() 구현
- userprog/syscall.c
void close(int fd) {
struct file *file = process_get_file(fd);
if (fd < 3 || file == NULL)
return;
process_close_file(fd);
file_close(file);
}
유저 프로세스가 open(fd)로 연 파일을 닫고, 그에 따른 파일 자원(struct file)을 해제하고 FDT(FD 테이블)에서 해당 엔트리를 비워주는 역할
struct file *file = process_get_file(fd);
- 현재 스레드의 파일 디스크립터 테이블(FDT)에서 fd에 해당하는 struct file * 포인터를 얻어옴
- 존재하지 않거나 닫힌 FD인 경우 file == NULL
if (fd < 3 || file == NULL)
return;
- fd < 3이면 stdin(0), stdout(1), stderr(2) → 시스템 예약 디스크립터
→ 일반 파일이 아니므로 close 할 필요 없음 - file == NULL: 해당 FD가 이미 닫혔거나 잘못된 값 → 아무것도 하지 않고 리턴
process_close_file(fd);
- FDT에서 fd에 해당하는 엔트리를 NULL로 설정해서 파일 디스크립터 테이블에서 제거
file_close(file);
- 실제 커널 수준에서 파일을 닫음
- 내부적으로는:
- 참조 카운트를 줄이고
- 파일이 더 이상 참조되지 않으면 메모리 해제
- 디스크 inode 캐시 업데이트 등 수행
page_fault() 수정
- create-bad-ptr, read-bad-ptr 등 15개의 test case에서 Page fault 발생
- Page fault 에러 메시지 출력으로 인해 test case가 fail 처리됨
- 에러 메시지 출력을 방지하기 위해 exit(-1)을 호출하도록 수정
- userprog/exception.c
static void page_fault (struct intr_frame *f) {
bool not_present; /* True: not-present page, false: writing r/o page. */
bool write; /* True: access was write, false: access was read. */
bool user; /* True: access by user, false: access by kernel. */
void *fault_addr; /* Fault address. */
/* Obtain faulting address, the virtual address that was
accessed to cause the fault. It may point to code or to
data. It is not necessarily the address of the instruction
that caused the fault (that's f->rip). */
fault_addr = (void *) rcr2();
/* Turn interrupts back on (they were only off so that we could
be assured of reading CR2 before it changed). */
intr_enable ();
/* Determine cause. */
not_present = (f->error_code & PF_P) == 0;
write = (f->error_code & PF_W) != 0;
user = (f->error_code & PF_U) != 0;
exit(-1); // 추가
#ifdef VM
/* For project 3 and later. */
if (vm_try_handle_fault (f, fault_addr, user, write, not_present))
return;
#endif
/* Count page faults. */
page_fault_cnt++;
/* 이 부분 주석 처리 */
/* If the fault is true fault, show info and exit. */
// printf ("Page fault at %p: %s error %s page in %s context.\n",
// fault_addr,
// not_present ? "not present" : "rights violation",
// write ? "writing" : "reading",
// user ? "user" : "kernel");
// kill (f);
}
'크래프톤 정글' 카테고리의 다른 글
[pintos] Week2~3: User Program Part.8 - exec, wait (0) | 2025.05.26 |
---|---|
[pintos] Week2~3: User Program Part.7 - fork (0) | 2025.05.26 |
[pintos] Week2~3: User Program Part.5 - 파일 디스크립터 (0) | 2025.05.25 |
[pintos] Week2~3: User Program Part.4 - halt, exit, create, remove (0) | 2025.05.20 |
[pintos] Week2~3: User Program Part.3 - 인자 파싱 (2) | 2025.05.20 |