Argument Passing
user 프로그램이 실행되기 전에 프로그램에 대한 인자를 설정
ex) "/bin/ls -l foo bar" 인자 처리
1. 명령어 분할 : /bin/ls, -l, foo, bar
2. 문자열과 null 포인터를 스택 오른쪽에서 왼쪽으로 푸시
- argv[0]를 가장 낮은 가상 주소에 위치
- 성능을 위해 첫 번째 푸시 전에 스택 포인터를 8의 배수로 내림하여 정렬
3. %rsi를 argv로 지정하고 %rdi를 argc로 설정
4. fake return address 푸시
1. command line을 parsing해서 file_name을 찾는다
tid_t process_create_initd(const char *file_name)
{
...
// Argument Passing ~
char *save_ptr;
strtok_r(file_name, " ", &save_ptr);
// ~ Argument Passing
return tid;
2. f_name을 parsing하고 user stack에 매개변수들을 push
+----------------------------------+
| argument_stack |
+----------------------------------+
| 1. Push "onearg" |
| +---+ |
| | \0| |
| +---+ |
| | g | |
| +---+ |
| | r | |
| +---+ |
| | a | |
| +---+ |
| | e | |
| +---+ |
| | n | |
| +---+ |
| | o | |
| +---+ |
+----------------------------------+
| 2. Push "args-single" |
| +---+ |
| | \0| |
| +---+ |
| | e | |
| +---+ |
| | l | |
| +---+ |
| | g | |
| +---+ |
| | n | |
| +---+ |
| | i | |
| +---+ |
| | s | |
| +---+ |
| | - | |
| +---+ |
| | s | |
| +---+ |
| | g | |
| +---+ |
| | r | |
| +---+ |
| | a | |
| +---+ |
+----------------------------------+
| 3. Push padding (0s if needed) |
| +---+ |
| | 0 | |
| +---+ |
+----------------------------------+
| 4. Push end marker (0) |
| +---+ |
| | 0 | |
| +---+ |
+----------------------------------+
| 5. Push address of "onearg" |
| +------------------+ |
| | addr of "onearg" | |
| +------------------+ |
+----------------------------------+
| 6. Push address of "args-single" |
| +-----------------------+ |
| | addr of "args-single" | |
| +-----------------------+ |
+----------------------------------+
| 7. Push return address (0) |
| +---+ |
| | 0 | |
| +---+ |
+----------------------------------+
int process_exec(void *f_name)
{
...
// Argument Passing ~
char *parse[64];
char *token, *save_ptr;
int count = 0;
for (token = strtok_r(file_name, " ", &save_ptr); token != NULL; token = strtok_r(NULL, " ", &save_ptr))
parse[count++] = token;
// ~ Argument Passing
...
// Argument Passing ~
argument_stack(parse, count, &_if.rsp); // 함수 내부에서 parse와 rsp의 값을 직접 변경하기 위해 주소 전달
_if.R.rdi = count;
_if.R.rsi = (char *)_if.rsp + 8;
hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)_if.rsp, true); // user stack을 16진수로 프린트
// ~ Argument Passing
...
}
3. process_exec() 함수에서 parsing한 프로그램 이름과 인자를 스택에 저장하기 위해 사용할 함수를 새로 선언
void argument_stack(char **parse, int count, void **rsp) // 주소를 전달받았으므로 이중 포인터 사용
{
// 프로그램 이름, 인자 문자열 push
for (int i = count - 1; i > -1; i--)
{
for (int j = strlen(parse[i]); j > -1; j--)
{
(*rsp)--; // 스택 주소 감소
**(char **)rsp = parse[i][j]; // 주소에 문자 저장
}
parse[i] = *(char **)rsp; // parse[i]에 현재 rsp의 값 저장해둠(지금 저장한 인자가 시작하는 주소값)
}
// 정렬 패딩 push
int padding = (int)*rsp % 8;
for (int i = 0; i < padding; i++)
{
(*rsp)--;
**(uint8_t **)rsp = 0; // rsp 직전까지 값 채움
}
// 인자 문자열 종료를 나타내는 0 push
(*rsp) -= 8;
**(char ***)rsp = 0; // char* 타입의 0 추가
// 각 인자 문자열의 주소 push
for (int i = count - 1; i > -1; i--)
{
(*rsp) -= 8; // 다음 주소로 이동
**(char ***)rsp = parse[i]; // char* 타입의 주소 추가
}
// return address push
(*rsp) -= 8;
**(void ***)rsp = 0; // void* 타입의 0 추가
}
File Descriptor
디렉토리, 소켓, 기타 입출력 장치 모두 파일의 형태로 취급하는데, 이 파일들을 구분하기 위해 쓰는 개념
프로세스들이 실행될때 file descriptor table도 함께 생성됨.
배열 형태로 관리되며, 각 element는 Inode table의 특정 위치 또는 연결된 소켓을 가리킴.
OS 관리
1. halt
2. exit
3. wait
4. fork
5. exec
File I/O System Call 목록
1. create
2. remove
3. open
4. filesize
5. read
6. write
7. seek
8. tell
9. close
각 스레드들은 고유한 file descriptor table을 가지고 있어야 한다.
스레드가 생성될 때 파일 테이블을 동적으로 할당.
파일 디스크립터 테이블 (fdt):
각 프로세스마다 갖고 있는 배열로, 열린 파일들에 대한 포인터를 담고 있음.
배열의 인덱스가 파일 디스크립터 번호 (fd)이며, 해당 fd를 통해 어떤 파일이 열려 있는지 알 수 있음.
switch case 문으로 핸들러 구현
| halt()
halt 함수는 pintos를 종료시키는 시스템 콜 함수이다. power_off()를 통해 간단하게 구현 가능하다.
// pintos 종료 시스템 콜
void halt(void) {
power_off();
}
| exit()
exit는 프로세스를 종료시키는 시스템 콜이다.
// 프로세스 종료 시스템 콜
void exit(int status) {
struct thread *cur = thread_current();
cur->exit_status = status; // 프로그램이 정상적으로 종료되었는지 확인(정상적 종료 시 0)
printf("%s: exit(%d)\n", thread_name(), status); // 종료 시 Process Termination Message 출력
thread_exit(); // 스레드 종료
}
thread_exit 호출해서 종료
thread_exit
- 인터럽트 비활성화하고 do_schedule 함수로 강제로 다른 스레드로 전환
| exec()
int exec(char *cmd_line)
{
// cmd_line이 유효한 사용자 주소인지 확인 -> 잘못된 주소인 경우 종료/예외 발생
check_address(cmd_line);
char *cmd_line_copy;
cmd_line_copy = palloc_get_page(PAL_ZERO);
if (cmd_line_copy == NULL)
exit(-1); // 메모리 할당 실패 시 status -1로 종료한다.
strlcpy(cmd_line_copy, cmd_line, PGSIZE); // cmd_line을 복사한다.
// 스레드의 이름을 변경하지 않고 바로 실행한다.
if (process_exec(cmd_line_copy) == -1)
exit(-1); // 실패 시 status -1로 종료한다.
}
process.c 파일의 process_create_initd 함수와 유사하다.
단, 스레드를 새로 생성하는 건 fork에서 수행하므로
이 함수에서는 새 스레드를 생성하지 않고 process_exec을 호출한다.
process_exec 함수 안에서 filename을 변경해야 하므로
커널 메모리 공간에 cmd_line의 복사본을 만든다.
(현재는 const char* 형식이기 때문에 수정할 수 없다.)
| create()
bool create(const char *file_created, unsigned initial_size)
{
check_address(file_created);
bool success = filesys_create(file_created, initial_size);
return success;
}
filesys_create(file_created, initial_size): 파일 시스템에서 file_created라는 이름의 파일을 생성하고, 해당 파일의 초기 크기를 initial_size로 설정합니다.
| remove()
bool remove(const char *file_removed)
{
check_address(file_removed);
bool success = filesys_remove(file_removed);
return success;
}
filesys_remove(file_removed): 파일 시스템에서 file_removed라는 이름의 파일을 삭제합니다.
| open()
int open(const char *file_opened)
{
// 파일이름이 유효한지 판단한다.
check_address(file_opened);
// 락을 걸어준다.
// 여러 프로세스가 동시에 파일 시스템에 접근하는 것을 막기 위해 락을 건다.
// 이 락은 파일을 여는 동안 파일 시스템의 동시 접근을 제어하는 역할을 한다.
lock_acquire(&filesys_lock);
// 파일 열기를 시도한다.
struct file *cur_file = filesys_open(file_opened);
// 파일 열기를 실패 시
if (cur_file == NULL)
{
lock_release(&filesys_lock);
return -1;
}
// 현재 스레드의 파일 디스크립터 테이블에 파일을 추가한다.
int fd = process_add_file(cur_file);
// 파일 디스크립터 할당에 실패하면 파일을 닫는다.
if (fd == -1)
{
file_close(cur_file);
}
// 처음에 건 락을 해제한다.
lock_release(&filesys_lock);
return fd;
}
파일을 열고 파일 디스크립터를 반환하는 역할
처음에 파일을 열기 위한 함수를 구현할 때,
파일 시스템 접근에 대한 동시성 문제를 해결하려고 lock_acquire()를 사용했지만,
락을 거는 위치에서 실수가 발생했다.
그 결과, 코드 실행 중 예상치 못한 동작이 발생하게 되었다.
예를 들어, 락을 filesys_open() 함수 호출 이후에 걸어버렸을 때,
파일 시스템에 동시에 접근하는 여러 스레드가 있으면,
두 스레드가 같은 파일에 동시에 접근해 경쟁 상태가 발생한다.
락이 너무 늦게 걸리면서 파일이 이미 열린 상태에서 다른 스레드가 파일을 변경하거나 삭제할 수 있기 때문인다.
이 문제를 해결하기 위해, 락을 파일을 여는 filesys_open() 함수 호출 전에 걸어주는 방식으로 변경했다.
이로써 여러 스레드가 동시에 파일 시스템에 접근할 때의 경쟁 상태를 방지할 수 있었고,
파일 시스템의 일관성을 유지할 수 있었습니다.
단순히 락을 사용만 하는게 아니라 정확히 알고 순서에 맞게 사용해야 한다는 것을 배웠습니다.
| filesize()
int filesize(int fd)
{
// 파일 디스크럽터 테이블 fd번째에 있는 파일을 가져온다.
struct file *cur_file = process_get_file(fd);
// 여기선 check_address()를 쓰면 안 된다.
// 파일 디스크럽터 테이블이 커널 영역에 있기 때문이다.
if (cur_file == NULL)
{
return -1;
}
return file_length(cur_file);
}
| read()
int read(int fd, void *buffer, unsigned size)
{
// STDOUT_FILENO : 1 -> 읽을 수 없다.
if (fd == STDOUT_FILENO)
return -1;
// 버퍼의 주소를 검증한다.
check_address(buffer);
// 데이터를 저장할 위치를 가리킨다.
char *ptr = (char *)buffer;
int bytes_read = 0;
// 파일 시스템 작업을 하는 동안, 락을 걸어준다.
// 현재 프로세스가 작업을 하는 도중, 다른 프로세스의 접근이 막힌다.
lock_acquire(&filesys_lock);
// STDIN_FILENO : 0 -> 한 문자씩 입력받아 buffer에 저장한다.
if (fd == STDIN_FILENO)
{
for (int i = 0; i < size; i++)
{
*ptr++ = input_getc();
bytes_read++;
}
}
// 표준 입력이 아닌 경우
// 파일 디스크립터에 연결된 파일을 가져온다.
// file_read(file, buffer, size)로, buffer로 읽어온다.
else
{
struct file *file = process_get_file(fd);
if (file == NULL)
{
lock_release(&filesys_lock);
return -1;
}
bytes_read = file_read(file, buffer, size);
}
// 락을 풀어주고, bytes_read를 반환한다.
lock_release(&filesys_lock);
return bytes_read;
}
이 함수는 파일이나 표준 입력에서 데이터를 읽어와 지정된 버퍼에 저장하는 역할을 하며,
멀티스레드 환경에서 안전하게 파일 시스템에 접근할 수 있도록 설계되었다.
# input_getc 함수로 한글자씩 버퍼에 저장하고
# 표준 입력이 아닐 경우 process_get_file(fd)로 가져온다.
// 표준 입력이 아닌 경우
else
{
struct file *file = process_get_file(fd);
if (file == NULL)
{
lock_release(&filesys_lock);
return -1;
위 함수에서 lock_release로 락을 해제하고 호출 실패를 반환하게 되어있는데,
문뜩 어차피 호출 실패인데 꼭 락을 해제해야 하나? 라는 생각이 들었다.
(마치 컴퓨터의 강제종료 처럼)
핀토스에서는 return -1이 단순히 종료를 의미하지 않으며,
다른 스레드와의 자원 관리를 고려하여 적절히 처리하는 것이 중요하다.
프로그램의 강제 종료와는 다르게, 운영 체제는 모든 프로세스의 안정성과 자원 관리를 유지해야 한다.
| write()
int write(int fd, void *buffer, unsigned size)
{
// STDIN_FILENO : 0 -> 쓸 수 없다.
if (fd == STDIN_FILENO)
return -1;
// 버퍼의 주소를 검증한다.
check_address(buffer);
int bytes_write = 0;
// 파일 시스템 작업을 하는 동안, 락을 걸어준다.
// 현재 프로세스가 작업을 하는 도중, 다른 프로세스의 접근이 막힌다.
lock_acquire(&filesys_lock);
// STDOUT_FILENO : 1 -> size 만큼 buffer에 저장한다.
if (fd == STDOUT_FILENO)
{
putbuf(buffer, size);
bytes_write = size;
}
// 표준 출력이 아닌 경우
// 파일 디스크립터에 연결된 파일을 가져온다.
// file_write(file, buffer, size)로, buffer에 있는 데이터를 file에 쓴다.
else
{
struct file *file = process_get_file(fd);
if (file == NULL)
{
lock_release(&filesys_lock);
return -1;
}
bytes_write = file_write(file, buffer, size);
}
// 락을 풀어주고, bytes_write를 반환한다.
lock_release(&filesys_lock);
return bytes_write;
}
표준 출력(STDOUT_FILENO, 1)인 경우,
putbuf 함수를 호출하여 buffer의 내용을 콘솔에 출력. 이 경우 bytes_write는 size로 설정
표준 출력이 아닌 경우,
지정된 파일 디스크립터에 연결된 파일을 가져옴.
만약 파일이 유효하지 않다면(즉, file이 NULL일 경우) 락을 해제하고 -1을 반환.
유효한 파일일 경우, file_write 함수를 호출하여 buffer의 내용을 해당 파일에 씀.
| seek()
void seek(int fd, unsigned position)
{
// 파일 디스크립터에 연결된 파일을 가져온다.
struct file *file = process_get_file(fd);
if (file != NULL)
// file.c의 file_seek()를 활용한다.
file_seek(file, 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 필드를 new_pos로 설정하여 파일의 읽기/쓰기 위치를 변경.
파일에서 다음에 읽거나 쓸 위치를 결정하는 데 사용
| tell()
unsigned tell(int fd)
{
// 파일 디스크립터에 연결된 파일을 가져온다.
struct file *file = process_get_file(fd);
if (file != NULL)
// file.c의 file_tell()을 활용한다.
file_tell(file);
}
파일 디스크립터에 대한 현재 파일 읽기/쓰기 위치를 반환하는 함수
| close()
void close(int fd)
{
// 프로세스에서 fd로 열려있는 파일을 닫는다.
process_close_file(fd);
}
void process_close_file(int fd)
{
struct thread *curr = thread_current();
struct file **fdt = curr->fdt;
// fd가 유효 범위인지?
if (fd < 2 || fd >= FDT_COUNT_LIMIT)
return;
if (fdt[fd] != NULL) // 해당 파일이 열려있는지?
{
file_close(fdt[fd]); // 파일 닫기
fdt[fd] = NULL; // 파일 테이블에서 제거
}
}