pintOS project2: User Programs -system call
📖system call이란?
운영체제는 커널 모드, 유저 모드로 나뉘어 구동된다. 이때 프로그램 (user program)은 유저 모드 레벨에서 구동되는데, 파일을 읽거나, 쓰거나, 화면에 메세지를 출력하는 등의 커널 모드에서 구동되는 기능들을 호출하기위해 system call이라는 interface를 운영체제는 제공한다.
운영체제는 커널 모드, 유저 모드로 나뉘어 구동된다. 이때 프로그램 (user program)은 유저 모드 레벨에서 구동되는데, 파일을 읽거나, 쓰거나, 화면에 메세지를 출력하는 등의 커널 모드에서 구동되는 기능들을 호출하기위해 system call이라는 interface를 운영체제는 제공한다.
https://www.differencebetween.com/difference-between-user-mode-and-vs-kernel-mode/
그림.open() 시스템 콜을 호출한 사용자 응용의 처리
필요한 기능이나 시스템 환경에 따라 시스템 콜이 발생할 때 좀 더 많은 정보가 필요할 수 있다. 그러한 정보가 담긴 매개변수를 운영체제에 전달하기 위해서는 대략 3가지 정도의 방법이 있다.
-
매개변수를 CPU 레지스터 내에 전달한다. 이 경우에 매개변수의 갯수가 CPU 내의 총 레지스터 개수보다 많을 수 있다.
-
위와 같은 경우에 매개변수를 메모리에 저장하고 메모리의 주소가 레지스터에 전달된다. (아래 그림 참고)
-
매개변수는 프로그램에 의해 스택(stack)으로 전달(push) 될 수도 있다.
그림.레지스터 메모리를 사용한 매개변수를 전달
pintOS(kaist)의 경우 X86-64 주처리장치(CPU)를 사용하는 운영체제인데, 이는 64비트 값을 저장할 수 있는 16개의 범용 정수 레지스터를 보유하고 있다.
이곳에서 system call (줄여서 syscall)은
-
%rax 가 system call 번호를 담는다. 각 번호는 enum을 통해 특정 system call 함수로 정의 되어있다.
-
4번쨰 argument는 %rcx가 아니라 %r10에 담긴다. 따라서 argument들은 순서대로 %rdi, %rsi, %rdx, %r10, %r8, and %r9 에 담겨 syscall 함수로 넘어간다.
📑구현해야 할 system call
시스템 콜은 대략 6개의 그룹으로 묶어 분류 할 수 있다. 이 중 pintOS kaist project에서 구현해야하는 함수들을 리스트했다.
프로세스 제어 / process control
- halt
- exit
- wait
- fork
- exec
파일 조작 / file management
- create
- remove
- open
- close
- read
- write
- filesize
- seek
- tell
- dup2
장치 관리 / device management
- read
- write
정보 유지 / information maintenance
- pintOS project에서는 구현 목표는 아님
통신 / communication
- pintOS project에서는 구현 목표는 아님
보호, 보안 / protection
- pintOS project에서는 구현 목표는 아님
https://en.wikipedia.org/wiki/System_call
💻syscall.c
/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
// TODO: Your implementation goes here.
// printf("system call!\n");
/* ==================== project2 system call ==================== */
char *fn_copy;
int siz;
switch (f->R.rax)
{
case SYS_HALT:
printf("halt!\n");
halt();
break;
case SYS_EXIT:
// printf("exit!\n");
exit(f->R.rdi);
break;
case SYS_FORK:
printf("fork!\n");
// f->R.rax = fork(f->R.rdi, f);
break;
case SYS_EXEC:
printf("exec!\n");
// if (exec(f->R.rdi) == -1)
// exit(-1);
break;
case SYS_WAIT:
printf("wait!\n");
// f->R.rax = process_wait(f->R.rdi);
break;
case SYS_CREATE:
// printf("create!\n");
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
// printf("remove!\n");
f->R.rax = remove(f->R.rdi);
break;
case SYS_OPEN:
// printf("open!\n");
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
printf("filesize!\n");
// f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
printf("read!\n");
// f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
// printf("write!\n");
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
printf("seek!\n");
// seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
printf("tell!\n");
// f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
printf("close!\n");
// close(f->R.rdi);
break;
case SYS_DUP2:
printf("dup2!\n");
// f->R.rax = dup2(f->R.rdi, f->R.rsi);
break;
default:
// printf("default exit!\n");
exit(-1);
break;
}
/* ==================== project2 system call ==================== */
// thread_exit ();
}
📑프로세스 제어 system call
📟halt()
void halt (void);
/* The main system call interface */
void
syscall_handler (struct intr_frame *f UNUSED) {
// TODO: Your implementation goes here.
// printf("system call!\n");
/* ==================== project2 system call ==================== */
char *fn_copy;
int siz;
switch (f->R.rax)
{
case SYS_HALT:
printf("halt!\n");
halt();
break;
case SYS_EXIT:
// printf("exit!\n");
exit(f->R.rdi);
break;
case SYS_FORK:
printf("fork!\n");
// f->R.rax = fork(f->R.rdi, f);
break;
case SYS_EXEC:
printf("exec!\n");
// if (exec(f->R.rdi) == -1)
// exit(-1);
break;
case SYS_WAIT:
printf("wait!\n");
// f->R.rax = process_wait(f->R.rdi);
break;
case SYS_CREATE:
// printf("create!\n");
f->R.rax = create(f->R.rdi, f->R.rsi);
break;
case SYS_REMOVE:
// printf("remove!\n");
f->R.rax = remove(f->R.rdi);
break;
case SYS_OPEN:
// printf("open!\n");
f->R.rax = open(f->R.rdi);
break;
case SYS_FILESIZE:
printf("filesize!\n");
// f->R.rax = filesize(f->R.rdi);
break;
case SYS_READ:
printf("read!\n");
// f->R.rax = read(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_WRITE:
// printf("write!\n");
f->R.rax = write(f->R.rdi, f->R.rsi, f->R.rdx);
break;
case SYS_SEEK:
printf("seek!\n");
// seek(f->R.rdi, f->R.rsi);
break;
case SYS_TELL:
printf("tell!\n");
// f->R.rax = tell(f->R.rdi);
break;
case SYS_CLOSE:
printf("close!\n");
// close(f->R.rdi);
break;
case SYS_DUP2:
printf("dup2!\n");
// f->R.rax = dup2(f->R.rdi, f->R.rsi);
break;
default:
// printf("default exit!\n");
exit(-1);
break;
}
/* ==================== project2 system call ==================== */
// thread_exit ();
}
void halt (void);
Terminates Pintos by calling power_off() (declared in src/include/threads/init.h).
This should be seldom(드물게) used, because you lose some information about possible deadlock situations, etc.
case SYS_HALT:
printf("halt!\n");
halt();
break;
// Terminates Pintos by calling power_off(). No return.
void halt(void)
{
power_off();
}
🛑📟exit()
void exit (int status);
Terminates the current user program, returning status to the kernel.
If the process's parent waits for it (see below), this is the status that will be returned. Conventionally(통상적으로), a status of 0 indicates success and nonzero values indicate errors.
case SYS_EXIT:
// printf("exit!\n");
exit(f->R.rdi);
break;
// End current thread, record exit statusNo return.
void exit(int status)
{
struct thread *cur = thread_current();
// cur->exit_status = status;
printf("%s: exit(%d)\n", thread_name(), status); // Process Termination Message
thread_exit();
}
thread_exit() 함수는 process_exit()을 요청한다.
//process_exit
🛑📟fork()
pid_t fork (const char *thread_name);
Create new process which is the clone of current process with the name THREAD_NAME.
You don't need to clone the value of the registers except %RBX, %RSP, %RBP, and %R12 - %R15, which are callee-saved registers.
Must return pid of the child process, otherwise shouldn't be a valid pid.
In child process, the return value should be 0. The child should have DUPLICATED resources including file descriptor and virtual memory space.
Parent process should never return from the fork until it knows whether the child process successfully cloned. That is, if the child process fail to duplicate the resource, the fork () call of parent should return the TID_ERROR.
The template utilizes the pml4_for_each() in threads/mmu.c to copy entire user memory space, including corresponding pagetable structures, but you need to fill missing parts of passed pte_for_each_func (See virtual address).
// (parent) Returns pid of child on success or -1 on fail
// (child) Returns 0
tid_t fork(const char *thread_name, struct intr_frame *f)
{
return process_fork(thread_name, f);
}
🛑📟wait()
int wait (pid_t pid);
Waits for a child process pid and retrieves the child's exit status.
If pid is still alive, waits until it terminates. Then, returns the status that pid passed to exit.
If pid did not call exit(), but was terminated by the kernel (e.g. killed due to an exception), wait(pid) must return -1. It is perfectly legal for a parent process to wait for child processes that have already terminated by the time the parent calls wait, but the kernel must still allow the parent to retrieve its child’s exit status, or learn that the child was terminated by the kernel.
wait must fail and return -1 immediately if any of the following conditions is true:
-
pid does not refer to a direct child of the calling process. pid is a direct child of the calling process if and only if the calling process received pid as a return value from a successful call to fork. Note that children are not inherited: if A spawns child B and B spawns child process C, then A cannot wait for C, even if B is dead. A call to wait(C) by process A must fail. Similarly, orphaned processes are not assigned to a new parent if their parent process exits before they do.
-
The process that calls wait has already called wait on pid. That is, a process may wait for any given child at most once.
Processes may spawn any number of children, wait for them in any order, and may even exit without having waited for some or all of their children. Your design should consider all the ways in which waits can occur. All of a process's resources, including its struct thread, must be freed whether its parent ever waits for it or not, and regardless of whether the child exits before or after its parent.
You must ensure that Pintos does not terminate until the initial process exits. The supplied Pintos code tries to do this by calling process_wait() (in userprog/process.c) from main() (in threads/init.c). We suggest that you implement process_wait() according to the comment at the top of the function and then implement the wait system call in terms of process_wait().
Implementing this system call requires considerably more work than any of the rest.
📑파일 조작 system call
stdin, stdout
The standard input device, also referred to as stdin, is the device from which input to the system is taken. Typically this is the keyboard, but you can specify that input is to come from a serial port or a disk file, for example.
The standard output device, also referred to as stdout, is the device to which output from the system is sent. Typically this is a display, but you can redirect output to a serial port or a file.
📟create()
새로운 파일을 만듭니다. 인자로 주어진 'file' 이름을 가진, 'initial_size' byte 길이의 파일을.
bool create (const char *file, unsigned initial_size);
Creates a new file called file, initially initial_size bytes in size. Returns true if successful, false otherwise.
Creating a new file does not open it: opening the new file is a separate operation which would require a open system call.
// Creates a new file called 'file', initially 'initial_size' bytes in size.
// Returns true if successful, false otherwise
bool create(const char *file, unsigned initial_size)
{
check_address(file);
//pintos-kaist/filesys/filesys.c
return filesys_create(file, initial_size);
}
📟remove()
파일을 삭제합니다. 인자로 받은 'file' 과 같은 이름인.
bool remove (const char *file);
Deletes the file called file. Returns true if successful, false otherwise.
A file may be removed regardless of whether it is open or closed, and removing an open file does not close it. See Removing an Open File in FAQ for details.
// Deletes the file called 'file'. Returns true if successful, false otherwise.
bool remove(const char *file)
{
check_address(file);
//pintos-kaist/filesys/filesys.c
return filesys_remove(file);
}
📟open()
int open (const char *file);
Opens the file called file. Returns a nonnegative integer handle called a "file descriptor" (fd), or -1 if the file could not be opened.
File descriptors numbered 0 and 1 are reserved for the 🛑console: fd 0 (STDINFILENO) is standard input, fd 1 (STDOUT_FILENO) is standard output. _The open system call will never return either of these file descriptors, which are valid as system call arguments only as explicitly described below.
Each process has an independent set of file descriptors. File descriptors are inherited(상속된다) by child processes. When a single file is opened more than once, whether by a single process or different processes, each open returns a new file descriptor.
Different file descriptors for a single file are closed independently in separate calls to close and they do not share a file position. You should follow the linux scheme, which returns integer starting from zero, to do the extra.
// Opens the file called file, returns fd or -1 (if file could not be opened for some reason)
int open(const char *file)
{
check_address(file);
//Opens the file with the given NAME. returns the new file if successful or a null pointer otherwise.
struct file *fileobj = filesys_open(file);
// fails if no file named NAME exists, or if an internal memory allocation fails.
if (fileobj == NULL)
return -1;
//allocate file to current process fdt
int fd = add_file_to_fdt(fileobj);
// FD table full
if (fd == -1)
file_close(fileobj);
return fd;
}
📟close()
void close (int fd);
Closes file descriptor fd. Exiting or terminating a process implicitly closes all its open file descriptors, as if by calling this function for each one.
// Closes file descriptor fd. Ignores NULL file. Returns nothing.
void close(int fd)
{
// file 주소를 fd 와 find_file_by_fd()로 찾기
struct file *fileobj = find_file_by_fd(fd);
if (fileobj == NULL)
return;
struct thread *cur = thread_current();
//fd 0, 1은 각각 stdin, stdout.
if (fd == 0 || fileobj == STDIN)
{
cur->stdin_count--;
}
else if (fd == 1 || fileobj == STDOUT)
{
cur->stdout_count--;
}
// fd table에서 [fd]의 값을 NULL로 초기화
remove_file_from_fdt(fd);
//만약 stdin, stdout 호출이였으면 여기서 마무리
if (fd <= 1 || fileobj <= 2)
return;
//fd가 일반 파일을 가르킬 경우 file_close 호출
file_close(fileobj);
// //하나의 파일에 두개 이상의 fd가 할당되었는지 검증.
// if (fileobj->dupCount == 0)
// file_close(fileobj);
// else
// fileobj->dupCount--;
}
📟read()
int read (int fd, void *buffer, unsigned size);
Reads size bytes from the file, open as fd, into buffer.
fd를 통해 size 만큼 file을 읽고 buffer에 저장한다.
Returns the number of bytes actually read (0 at end of file), or -1 if the file could not be read (due to a condition other than end of file).
fd 0 reads from the keyboard using input_getc().
// Reads size bytes from the file open as fd into buffer.
// Returns the number of bytes actually read (0 at end of file), or -1 if the file could not be read
int read(int fd, void *buffer, unsigned size)
{
check_address(buffer);
int ret;
struct thread *cur = thread_current();
struct file *fileobj = find_file_by_fd(fd);
if (fileobj == NULL)
return -1;
// fd 0 reads from the keyboard using input_getc().
// 왜 fd == 0 인 조건은 안될까?
if (fd == 0 || fileobj == STDIN)
{
// stdin device와의 연결이 해제(close)되어 있을 경우 stdin_count == 0
if (cur->stdin_count == 0)
{
// Not reachable
NOT_REACHED();
remove_file_from_fdt(fd);
ret = -1;
}
else
{
int i;
unsigned char *buf = buffer;
for (i = 0; i < size; i++)
{
// input_getc는 한글자 씩 buffer에서 혹은 buffer가 비었다면 key가 눌리길 기다린다.
char c = input_getc();
// 주소를 1씩 올려가며 차례대로 buffer에 한글자씩 담는다.
*buf++ = c;
if (c == '\0')
break;
}
ret = i;
}
}
else if (fd == 1 || fileobj == STDOUT)
{
ret = -1;
}
else //일반적인 파일을 읽는다면
{
// file_rw_lock defined in syscall.h
// Q. read는 동시접근 허용해도 되지 않을까?
lock_acquire(&file_rw_lock);
// Reads SIZE bytes from FILE into BUFFER
ret = file_read(fileobj, buffer, size);
lock_release(&file_rw_lock);
}
return ret;
}
📟filesize()
int filesize (int fd);
Returns the size, in bytes, of the file open as fd.
// Returns the size, in bytes, of the file open as fd.
int filesize(int fd)
{
struct file *fileobj = find_file_by_fd(fd);
if (fileobj == NULL)
return -1;
/* Returns the size of FILE in bytes. */
return file_length(fileobj);
}
📟write()
int write (int fd, const void *buffer, unsigned size);
Writes size bytes from buffer to the open file fd.
fd 파일에 buffer에서 size 바이트 만큼 쓴다.
Returns the number of bytes actually written, which may be less than size if some bytes could not be written.
Writing past end-of-file would normally extend the file, but file growth is not implemented by the basic file system. The expected behavior is to write as many bytes as possible up to end-of-file and return the actual number written, or 0 if no bytes could be written at all.
fd 1 writes to the console. Your code to write to the console should write all of buffer in one call to putbuf(), at least as long as size is not bigger than a few hundred bytes (It is reasonable to break up larger buffers). Otherwise, lines of text output by different processes may end up interleaved on the console, confusing both human readers and our grading scripts.
📟seek()
void seek (int fd, unsigned position);
Changes the next byte to be read or written in open file fd to position, expressed in bytes from the beginning of the file (Thus, a position of 0 is the file's start).
A seek past the current end of a file is not an error. A later read obtains 0 bytes, indicating end of file. A later write extends the file, filling any unwritten gap with zeros. (However, in Pintos files have a fixed length until project 4 is complete, so writes past end of file will return an error.)
These semantics are implemented in the file system and do not require any special effort in system call implementation.
// Changes the next byte to be read or written in open file fd to position,
// expressed in bytes from the beginning of the file (Thus, a position of 0 is the file's start).
void seek(int fd, unsigned position)
{
struct file *fileobj = find_file_by_fd(fd);
// stdin, stdout 은 무시
if (fileobj <= 2)
return;
// fileobj->pos = position;
}
💻(참고) file.c /struct file, file_read(), file_write()
/* An open file. */
struct file {
struct inode *inode; /* File's inode. */
off_t pos; /* Current position. */
bool deny_write; /* Has file_deny_write() been called? */
};
off_t
file_read (struct file *file, void *buffer, off_t size) {
off_t bytes_read = inode_read_at (file->inode, buffer, size, file->pos);
file->pos += bytes_read;
return bytes_read;
}
off_t
file_write (struct file *file, const void *buffer, off_t size) {
off_t bytes_written = inode_write_at (file->inode, buffer, size, file->pos);
file->pos += bytes_written;
return bytes_written;
}
📟tell()
unsigned tell (int fd);
Returns the position of the next byte to be read or written in open file fd, expressed in bytes from the beginning of the file.
// Returns the position of the next byte to be read or written in open file fd, expressed in bytes from the beginning of the file.
unsigned tell(int fd)
{
struct file *fileobj = find_file_by_fd(fd);
// stdin, stdout 은 무시
if (fileobj <= 2)
return;
return file_tell(fileobj);
}
⛩syscall 지원 함수 및 thread.h
💻thread.h
struct thread {
/* Owned by thread.c. */
tid_t tid; /* Thread identifier. */
enum thread_status status; /* Thread state. */
char name[16]; /* Name (for debugging purposes). */
int priority; /* Priority. */
/* Shared between thread.c and synch.c. */
struct list_elem elem; /* List element. */
/* ==================== project1 ==================== */
/* priority donation */
/* priority를 양도 받고 반환하고나서 원래 priority를 복원하기 위해 기록*/
int init_priority;
/* thread가 현재 얻기 위해 기다리고 있는 lock 으로 스레드는 이 lock 이 release 되기를 기다린다. */
struct lock *wait_on_lock;
/* 자신에게 priority 를 나누어준 thread들의 리스트이고 */
struct list donations;
/* donations 리스트를 관리하기 위한 element 로 thread 구조체의 그냥 elem 과 구분하여 사용하도록 한다. */
struct list_elem donation_elem;
/* Wake Up Time Tick (시스템이 시작된 이후부터 언제 일어나야되는지 알려주는 시간) */
int64_t wake_ticks;
/* ==================== project1 ==================== */
/* ==================== project2 ==================== */
// 2-3 Parent-child hierarchy
struct list child_list; // keep children
struct list_elem child_elem; // used to put current thread into 'children' list
// 2-3 wait syscall
// struct semaphore wait_sema; // used by parent to wait for child
int exit_status; // used to deliver child exit_status to parent
// 2-3 fork syscall
struct intr_frame parent_if; // to preserve my current intr_frame and pass it down to child in fork ('parent_if' in child's perspective)
// struct semaphore fork_sema; // parent wait (process_wait) until child fork completes (__do_fork)
// struct semaphore free_sema; // Postpone child termination (process_exit) until parent receives its exit_status in 'wait' (process_wait)
// 2-4 file descripter
struct file **fdTable; // allocation in threac_create (thread.c)
int fdIdx; // an index of an open spot in fdTable
// 2-5 deny exec writes
struct file *running; // executable ran by current process (process.c load, process_exit)
// 2-extra - count the number of open stdin/stdout
// dup2 may copy stdin or stdout; stdin or stdout is not really closed until these counts goes 0
int stdin_count;
int stdout_count;
/* ==================== project2 ==================== */
#ifdef USERPROG
/* Owned by userprog/process.c. */
uint64_t *pml4; /* Page map level 4 */
#endif
#ifdef VM
/* Table for whole virtual memory owned by thread. */
struct supplemental_page_table spt;
#endif
/* Owned by thread.c. */
struct intr_frame tf; /* Information for switching */
unsigned magic; /* Detects stack overflow. */
};
📟check_address(const uint64_t *uaddr)
인자로 받는 주소(포인터)를 검사합니다.
- null이거나
- Kernel VM를 가르키거나
- mapping 되지 않은 VM을 가르키면
user program(를 실행 중인 프로세스, 쓰레드)를 종료 시킵니다.
// Check validity of given user virtual address. Exits if any of below conditions is met.
// 1. Null pointer
// 2. A pointer to kernel virtual address space (above KERN_BASE)
// 3. A pointer to unmapped virtual memory (causes page_fault)
void check_address(const uint64_t *uaddr)
{
struct thread *cur = thread_current();
if (uaddr == NULL || !(is_user_vaddr(uaddr)) || pml4_get_page(cur->pml4, uaddr) == NULL)
{
exit(-1);
}
}
📟find_file_by_fd(int fd)
fdtable 리스트에 fd 번째에 (fdTable[fd]) file 주소가 저장되어있기에 이를 return 해준다.
// Project 2-4. File descriptor
// Check if given fd is valid, return cur->fdTable[fd]
static struct file *find_file_by_fd(int fd)
{
struct thread *cur = thread_current();
// Error - invalid fd
if (fd < 0 || fd >= FDCOUNT_LIMIT)
return NULL;
return cur->fdTable[fd]; // automatically returns NULL if empty
}
📟add_file_to_fdt(struct file *file)
현재 thread의 file descriptor table 에서 빈자리를 찾고, 인자로 받은 파일에게 file descriptor을 할당해줍니다. fd를 리턴합니다.
// Find open spot in current thread's fdt and put file in it. Returns the fd.
// fdt = file descriptor table
int add_file_to_fdt(struct file *file)
{
struct thread *cur = thread_current();
struct file **fdt = cur->fdTable; // file descriptor table
/* Project2-extra - (multi-oom) Find open spot from the front
* 1. 확보가능한 fd 번호 (fdIdx)가 limit 보다 작고,
* 2. fdt[x] 에 값이 있다면 while문 계속 진행
* 결과적으로 fdt[x]가 NULL값을 리턴 할 때 while 문을 탈출한다. = 빈 자리. */
while ((cur->fdIdx < FDCOUNT_LIMIT) && fdt[cur->fdIdx])
cur->fdIdx++;
// Error - fdt full
if (cur->fdIdx >= FDCOUNT_LIMIT)
return -1;
// 빈 fd에 file의 주소를 기록해준다.
fdt[cur->fdIdx] = file;
return cur->fdIdx;
}
📟remove_file_from_fdt(int fd)
fdTable의 fd 번째에(fdTable[fd]) 저장된 값을 NULL로 초기화시켜준다.
// Check for valid fd and do cur->fdTable[fd] = NULL. Returns nothing
void remove_file_from_fdt(int fd)
{
struct thread *cur = thread_current();
// Error - invalid fd
if (fd < 0 || fd >= FDCOUNT_LIMIT)
return;
cur->fdTable[fd] = NULL;
}
Author And Source
이 문제에 관하여(pintOS project2: User Programs -system call), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@bdbest72/pintOS-project2-User-Programs-system-call저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)