sys_filesize
목표
filesize(int fd)
→ 열린 파일 디스크립터 fd의 파일 크기를 byte 단위로 반환
read(int fd, void *buffer, unsigned size)
→ 파일 디스크립터 fd에서 최대 size만큼 읽어 buffer에 저장, 실제 읽은 byte 수 반환
→ EOF이면 0, 실패 시 -1
→ fd 0이면 키보드에서 입력
시스템 콜 처리 전체 흐름
1. 유저 프로그램에서 syscall 호출
- 예: read(fd, buf, 128); → 유저 라이브러리에서 syscall 어셈블리 명령 실행
2. 커널 진입 → syscall_handler(struct intr_frame f)
- 레지스터에서 인자 추출:
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;
3. syscall 번호로 분기
switch (syscall_num) {
...
case SYS_READ:
f->R.rax = sys_read(arg1, arg2, arg3);
break;
case SYS_FILESIZE:
f->R.rax = sys_filesize(arg1);
break;
}
int filesize(int fd); 구현 로직
// 시스템 콜: 파일 디스크립터(fd)에 해당하는 열린 파일의 크기를 바이트 단위로 반환한다.
// 실패 시 -1 반환.
int sys_filesize(int fd)
{
// 현재 실행 중인 스레드(프로세스)의 파일 디스크립터 테이블을 가져온다.
struct thread *cur = thread_current();
// fd가 음수이거나 유효 범위를 벗어난 경우 에러 처리
if (fd < 0 || fd >= MAX_FD)
{
return -1;
}
// 해당 fd에 해당하는 열린 파일을 가져온다.
struct file *file_obj = cur->fd_table[fd];
// 해당 fd에 열린 파일이 없으면 -1 반환
if (file_obj == NULL)
{
return -1;
}
// 파일의 실제 크기를 가져온다. 내부적으로 inode->data.length 값을 반환함
off_t size = file_length(file_obj);
// 파일 크기를 반환
return size;
}
함수 호출 흐름
- file_length() → 내부적으로 inode_length() 호출
- inode_length() → inode->data.length 값 직접 반환 (정확한 바이트 수)
off_t file_length(struct file *file);
// file_length: struct file가 가리키는 파일의 크기를 바이트 단위로 반환한다.
// 내부적으로 해당 파일이 참조하는 inode의 길이(inode->data.length)를 반환함.
off_t
file_length (struct file *file) {
// file 포인터가 NULL이 아닌지 확인 (디버깅 시점에서 문제 파악용)
ASSERT (file != NULL);
// 해당 파일이 참조하는 inode의 길이를 반환한다.
// inode_length는 inode->data.length를 반환하는 함수다.
return inode_length (file->inode);
}
- struct file은 파일 객체이며, 내부에 struct inode *inode가 있음
- inode_length(inode)는 해당 inode의 실제 크기 (inode->data.length)를 반환
- 이 함수는 sys_filesize() 시스템 콜의 핵심 호출 지점이기도 함
struct file 구조체
/* An open file. 커널이 파일을 열 때 사용하는 파일 객체 구조체 */
struct file {
struct inode *inode; // 이 파일이 가리키는 inode (실제 디스크 상의 파일 정보)
off_t pos; // 현재 파일에서 읽거나 쓸 위치 (byte 단위 offset)
bool deny_write; // 파일에 대한 쓰기 금지 여부. true면 write 불가
};
- 이건 디스크 위의 “파일 그 자체”가 아니라,
- “파일을 여는 동작에 따라 생성된 핸들(handle)” 같은 존재
→ 즉, 같은 inode를 여러 개의 file이 가리킬 수 있음
→ file->pos는 열린 파일마다 다를 수 있음 (파일 디스크립터마다 커서가 다름)
off_t inode_length(const struct inode *inode);
/* Returns the length, in bytes, of INODE's data.
즉, 디스크 상에 존재하는 파일의 전체 크기(바이트 단위)를 반환한다. */
off_t
inode_length (const struct inode *inode) {
// inode는 파일의 메타데이터를 담고 있는 구조체이며,
// 그 내부의 data.length가 실제 파일의 전체 크기를 나타낸다.
return inode->data.length;
}
- struct inode는 파일 시스템에서 디스크 상의 파일 자체를 나타냄
- inode->data.length는 해당 파일이 디스크 상에 차지하는 바이트 수 (예: 373 bytes)
내가 정리해본 흐름
- 시스템 콜 filesize(fd)는 현재 스레드의 파일 디스크립터 테이블에서 fd에 해당하는 열린 파일을 찾고, 해당 파일의 크기를 구해 반환한다.
- 이때 크기를 표현하기 위해 off_t 타입을 사용하는데, 이는 POSIX 시스템에서 파일 오프셋과 크기를 나타내기 위해 사용되는 표준 정수형 타입이다.
- 내부적으로는 file_length() → inode_length() → inode->data.length 순으로 접근해 실제 크기를 바이트 단위로 얻는다.
- inode는 디스크 상 파일의 메타데이터 구조체로, data.length는 해당 파일의 전체 바이트 크기를 담고 있다.
sys_read
int sys_read(int fd, void *buffer, unsigned size); 구현 로직
- 열린 파일 디스크립터 fd로부터
- 최대 size 바이트를 읽어
- 유저 영역의 buffer에 저장
- 실제로 읽은 바이트 수를 반환, 실패 시 -1 또는 프로세스 종료
// 시스템 콜: 파일 디스크립터 fd에서 최대 size만큼 데이터를 읽어 buffer에 저장.
// 성공 시 실제로 읽은 바이트 수를 반환, 실패 시 -1 또는 종료 처리.
int sys_read(int fd, void *buffer, unsigned size) {
// size가 0이면 읽을 게 없으므로 0 반환
if (size == 0)
return 0;
// 유저 메모리 접근 보호: buffer ~ buffer + size 전 범위가 유저 영역인지 검사
for (size_t i = 0; i < size; i++) {
uint8_t *addr = (uint8_t *)buffer + i;
if (!is_user_vaddr(addr) || pml4_get_page(thread_current()->pml4, addr) == NULL)
sys_exit(-1); // 잘못된 주소: 프로세스 종료
}
struct thread *cur = thread_current();
// fd가 유효하지 않으면 -1 반환
if (fd < 0 || fd >= MAX_FD) {
return -1;
}
// fd == 0 → 표준 입력 (키보드) 처리
if (fd == 0) {
// input_getc()로 한 글자씩 읽어서 buffer에 저장
for (unsigned i = 0; i < size; i++) {
((char *)buffer)[i] = input_getc();
}
return size; // 실제로 size만큼 읽었으므로 size 반환
}
// 일반 파일 처리: fd 테이블에서 열린 파일 구조체를 가져온다
struct file *file_obj = cur->fd_table[fd];
if (file_obj == NULL) {
return -1; // 해당 fd에 열린 파일이 없음
}
// 🔐 파일 시스템 접근: 파일에서 데이터 읽기 (커서 위치부터 size만큼)
// 필요시 filesys_lock으로 보호해도 됨 (경우에 따라)
int bytes_read = file_read(file_obj, buffer, size);
return bytes_read; // 실제로 읽은 바이트 수 반환
}
size가 0이면 바로 종료
if (size == 0)
return 0;
유저 메모리 접근 보호
- 유저가 넘긴 주소 buffer가:
- 유저 영역 주소인지?
- 실제로 매핑된 페이지인지?
- 둘 다 확인해서 이상하면 프로세스 종료 (sys_exit(-1))
for (size_t i = 0; i < size; i++) {
uint8_t *addr = (uint8_t *)buffer + i;
if (!is_user_vaddr(addr) || pml4_get_page(thread_current()->pml4, addr) == NULL)
sys_exit(-1);
}
- 커널이 유저의 buffer 주소에 접근해야 하기 때문에,
- buffer ~ buffer + size까지 모든 바이트가 유저 영역에 매핑되어 있는지 확인함
- 메모리 오류, 공격, 잘못된 포인터에 대한 방어
fd 유효성 검사
if (fd < 0 || fd >= MAX_FD)
return -1;
- 파일 디스크립터는 반드시 0 이상 MAX_FD 미만의 정수여야 함
- 그렇지 않으면 잘못된 요청 → 에러
표준 입력 처리 (fd == 0)
if (fd == 0) {
for (unsigned i = 0; i < size; i++)
((char *)buffer)[i] = input_getc();
return size;
}
- fd == 0이면 키보드 입력을 뜻함
- input_getc()를 통해 사용자로부터 문자 입력을 size만큼 받아 buffer에 저장
- 읽은 바이트 수(size)를 그대로 반환
일반 파일에서 읽기
struct file *file_obj = cur->fd_table[fd];
if (file_obj == NULL)
return -1;
- 현재 프로세스의 파일 디스크립터 테이블에서 fd에 대응되는 struct file *을 가져옴
- NULL이라면, 파일이 열려 있지 않은 상태 → 에러 반환
파일에서 데이터 읽기
int bytes_read = file_read(file_obj, buffer, size);
return bytes_read;
- file_read() 함수는:
- 현재 file_obj->pos(파일 커서)부터
- 최대 size 바이트를 읽어서
- buffer에 저장하고
- 실제로 읽은 바이트 수를 반환함
- 읽은 만큼 file->pos는 자동으로 증가함
흐름
(1) size 검사 →
(2) buffer 주소 유효성 검사 →
(3) fd 유효성 검사 →
(4) stdin이면 키보드 입력 처리 →
(5) 일반 파일이면 fd_table에서 struct file* 가져옴 →
(6) file_read()로 읽기 수행
표준 입력 처리를 좀 더 깊게 보자
uint8_t input_getc(void)
uint8_t input_getc(void) {
enum intr_level old_level;
uint8_t key;
old_level = intr_disable(); // 인터럽트 비활성화 (atomic 보호)
key = intq_getc(&buffer); // 내부 키보드 큐에서 한 글자 꺼냄
serial_notify(); // 시리얼 디바이스에 알림 (디버깅용 or 확장용)
intr_set_level(old_level); // 인터럽트 원래 상태로 복원
return key; // 사용자가 입력한 문자 반환
}
intq_getc(struct intq *q)
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); // 입력이 들어올 때까지 대기 (sleep)
lock_release(&q->lock); // 락 해제
}
byte = q->buf[q->tail]; // 입력 버퍼에서 글자 꺼내기
q->tail = next(q->tail); // tail 인덱스 한 칸 이동
signal(q, &q->not_full); // 버퍼에 공간 생겼다고 알림
}
- 만약 buf에 아무 입력이 없다면?키보드 인터럽트가 들어올 때까지 대기
- → 유저 프로세스는 잠들고(sleep),
- 데이터가 들어오면 wake 되어 이어서 실행
큐 구조 요약 (struct intq)
→ 생산자-소비자 문제의 고전적 구조
→ 키보드 인터럽트 핸들러: 생산자
→ read(fd = 0): 소비자
read()가 문자 요청했을 때, 아직 키보드에서 입력 안 됐으면 기다려야 함 반대로, 입력이 먼저 도착해도 read()가 아직 안 불렀으면 → 큐에 저장해둬야 함 이걸 해결해주는 게 입력 큐(buffer)
struct intq {
uint8_t buf[BUF_SIZE]; // 원형 큐
int head; // 쓰는 위치
int tail; // 읽는 위치
sema_t not_empty; // 읽을 게 있다는 조건
sema_t not_full; // 쓸 수 있다는 조건
struct lock lock; // 보호용 락
};
키보드 입력 발생 (Interrupt)
- 사용자가 키보드로 ‘a’ 입력
- 키보드 인터럽트 발생
- 커널이 intq_putc(&buffer, 'a') 호출
- → 큐가 꽉 차지 않았으면 buf[head] = 'a' 저장
- → head 증가
유저가 read(0, buf, size) 호출
- 커널의 input_getc() 호출됨
- 내부적으로 intq_getc() 호출됨
- buf[tail]에서 문자 하나 꺼냄
- → tail 증가
그럼 반대로 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);
}
- 인터럽트 핸들러가 한 글자 입력을 받을 때 호출됨
- 버퍼가 차 있으면 (아주 드물게), 기다려야 함
- 입력이 들어오면 대기 중이던 프로세스를 깨움
PintOS에서 read()는 단순한 함수가 아니라 커널 내부에서 인터럽트 기반 입력 버퍼와 동기화 메커니즘(조건 변수, 세마포어 등)을 정교하게 연결한 구조랍니다
sys_seek
다음 읽기/쓰기 위치를 position으로 변경. 파일 끝을 넘어가도 오류 아님 • 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);
}
file_seek
void file_seek(struct file *file, off_t new_pos)
{
ASSERT(file != NULL);
ASSERT(new_pos >= 0);
file->pos = new_pos;
}
- file->pos는 현재 파일 내에서 읽기/쓰기가 진행되는 커서 위치
- 이 값을 바꾸면 다음 read()나 write() 호출 시 영향을 줌
sys_tell
현재 fd에서 다음 읽기/쓰기가 이루어질 위치(바이트 단위)를 반환
“다음 읽기/쓰기 위치”란, 말 그대로 read()나 write()를 호출하면 어디서부터 시작되는지를 알려주는 정보
/* 현재 열린 파일의 커서 위치를 바이트 단위로 반환하는 시스템 콜 */
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->pos 값을 리턴하는 단순한 함수
- file->pos는 현재 파일 내 커서 위치를 나타냄 (읽기/쓰기 위치)
'크래프톤정글 > Pintos' 카테고리의 다른 글
| User Program _System Call(테스트 별 정리_read-nomal, bad, syn, rox) (0) | 2025.05.25 |
|---|---|
| User Program _System Call(exit, write, open, close, create, remove) (0) | 2025.05.21 |
| User Program _ Argument Passing (1) | 2025.05.17 |
| User Program _ main부터 (0) | 2025.05.17 |
| PintOS_Priority_Scheduling_part_3_donation (1) | 2025.05.13 |