본문 바로가기
카테고리 없음

WIL 2

by lacuca9 2024. 10. 7.

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;			 // 파일 테이블에서 제거
	}
}