User Program _System Call(filesize, read, seek, tell)

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)

  1. 사용자가 키보드로 ‘a’ 입력
  2. 키보드 인터럽트 발생
  3. 커널이 intq_putc(&buffer, 'a') 호출
    • → 큐가 꽉 차지 않았으면 buf[head] = 'a' 저장
    • → head 증가

유저가 read(0, buf, size) 호출

  1. 커널의 input_getc() 호출됨
  2. 내부적으로 intq_getc() 호출됨
  3. 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는 현재 파일 내 커서 위치를 나타냄 (읽기/쓰기 위치)