본문 바로가기
정글/pintos

[Project2] User Program(2) - Passing the argument and creating a thread: process_exec 함수의 뿌리를 찾아서...

by 위대한초밥V 2023. 6. 5.

process_exec (void *f_name)는 현재 실행 컨텍스트를 f_name으로 바꾸는 함수이다. 

이 과제에서 우리는 process_exec에서 호출하는 file을 로드하는 load() 함수에서 f_name을 통해 들어온 명령어를 잘 파싱하여 파일 명과 인자를 구분하는 작업을 해야한다. 

 

Argument Parsing을 구현하기 위해 process_exec가 어디에서 나왔는지 거슬러 쭉 올라가면 main함수이다.

한번 위부터 내려와보겠다.


🔎 코드 흐름

int main(void) (threads/init.c)

/* Pintos main program. */
int
main (void) {
	uint64_t mem_end;
	char **argv;

	/* Clear BSS and get machine's RAM size. */
	bss_init ();

	/* Break command line into arguments and parse options. */
	argv = read_command_line ();
	argv = parse_options (argv);

	/* Initialize ourselves as a thread so we can use locks,
	   then enable console locking. */
	thread_init ();
	console_init ();

	/* Initialize memory system. */
	mem_end = palloc_init ();
	malloc_init ();
	paging_init (mem_end);

#ifdef USERPROG
	tss_init ();
	gdt_init ();
#endif

	/* Initialize interrupt handlers. */
	intr_init ();
	timer_init ();
	kbd_init ();
	input_init ();
#ifdef USERPROG
	exception_init ();
	syscall_init ();
#endif
	/* Start thread scheduler and enable interrupts. */
	thread_start ();
	serial_init_queue ();
	timer_calibrate ();

#ifdef FILESYS
	/* Initialize file system. */
	disk_init ();
	filesys_init (format_filesys);
#endif

#ifdef VM
	vm_init ();
#endif

	printf ("Boot complete.\n");

	/* Run actions specified on kernel command line. */
	run_actions (argv);

	/* Finish up. */
	if (power_off_when_done)
		power_off ();
	thread_exit ();
}

pintos의 메인 프로그램의 pintos가 시작되는 곳을 말한다. USERPROG 블록 안에 내용 위주로 확인한다.

 

+) 왜 main 함수는 threads 안에 있을까?🧐

pintos는 스레드를 중심으로 한 운영체제 프레임워크이다. init.c 에서 Pintos 시스템의 초기화를 수행하고, 사용자 프로그램을 실행하는 역할을 하므로 스레드 관리와 관련된 초기화 작업은 대부분 이 파일이 담당한다. 이처럼 핵심 기능은 스레드에 main 함수를 배치하면 운영체제의 핵심 기능을 잘 드러낼 수 있다.

 

void run_actions(char **argv) (threads/init.c)

static void
run_actions (char **argv) {
	/* An action. */
	struct action {
		char *name;                       /* Action name. */
		int argc;                         /* # of args, including action name. */
		void (*function) (char **argv);   /* Function to execute action. */
	};

	/* Table of supported actions. */
	static const struct action actions[] = {
		{"run", 2, run_task}, // 지금은 run action만 살펴본다.
#ifdef FILESYS
		{"ls", 1, fsutil_ls},
		{"cat", 2, fsutil_cat},
		{"rm", 2, fsutil_rm},
		{"put", 2, fsutil_put},
		{"get", 2, fsutil_get},
#endif
		{NULL, 0, NULL},
	};
	
	// while문으로 action 구조체를 탐색하면서 action name을 비교하면서 똑같으면 function을 실행한다.
	while (*argv != NULL) {
		const struct action *a;
		int i;

		/* Find action name. */
		for (a = actions; ; a++)
			// action이 존재하는지 확인
			if (a->name == NULL)
				PANIC ("unknown action `%s' (use -h for help)", *argv);
			else if (!strcmp (*argv, a->name))
				break;

		/* Check for required arguments. */
		for (i = 1; i < a->argc; i++)
			if (argv[i] == NULL)
				PANIC ("action `%s' requires %d argument(s)", *argv, a->argc - 1);

		/* Invoke action and advance. */
		a->function (argv);
		argv += a->argc;
	}

}

초기화할 대상을 초기화하고 kernel command line에 정의된 action을 실행할 때,  argv를 인자로 받는다.

만약, 명령이 pintos --fs-disk=10 -p tests/userprog/args-single:args-single -- -q -f run 'args-single onearg' 들어왔다면 main에서 파싱하여 run 'args-single onearg' 만 들어왔을 것이다. 즉 argv는 main에서 read_command_line()으로 읽어들인 후, parse_options()로 해당 line을 parsing하여 어떠한 action을 수행할지 argv 담아 인자로 넘기는 것이다.

(main에서 어떻게 parsing되는지 알고 싶다면, threads/init.c의 parse_options 함수 참고!!!📝)

 

실행할 action을 담고 있는 action 타입의 배열, actions를 생성한다.(현재 과제에서는 run 액션만 수행가능하다.)

인자로 들어온 action이름을 확인하면서, action이 존재하는지 확인하고 있다면, argc(argv의 개수)가  들어왔는지 검사한다. 

모든 검증이 되었다면 해당 action을 호출하여 진행한다.

void run_task(char **argv) (threads/init.c)

/* Runs the task specified in ARGV[1]. */
static void
run_task (char **argv) {
	const char *task = argv[1];

	printf ("Executing '%s':\n", task);
#ifdef USERPROG
	if (thread_tests){ 
		run_test (task);
	} else {
		process_wait (process_create_initd (task));
	}
#else
	run_test (task);
#endif
	printf ("Execution of '%s' complete.\n", task);
}

argv 배열의 0번째 인덱스에는 run 액션이, 1번째 인덱스에는 args-single onearg 작업이 담긴다.

작업을 process_create_initd 함수에 인자로 전달한다. 

 

tid_t process_create_initd (const char *file_name) (userprog/process.c)

tid_t
process_create_initd (const char *file_name) {
	char *fn_copy;
	tid_t tid;

	/* Make a copy of FILE_NAME.
	 * Otherwise there's a race between the caller and load(). */
	fn_copy = palloc_get_page (0);
	if (fn_copy == NULL)
		return TID_ERROR;
	strlcpy (fn_copy, file_name, PGSIZE);

	/* Create a new thread to execute FILE_NAME. */
	// echo x y z에서 echo만 들어가도록 한다.
	tid = thread_create (file_name, PRI_DEFAULT, initd, fn_copy); 
	// thread를 생성할 때, initd 함수를 호출한다.
	if (tid == TID_ERROR)
		palloc_free_page (fn_copy);
	return tid;
}

command line에서 받은 argument를 실행하기 위해 파일의 프로세스를 만든다.

run_task 함수로부터 호출되어, file_name은 'args-single onearg'를 갖을 것이다. 

palloc_get_page() 함수를 통해 page를 할당받고 해당 page에 file_name을 저장한다. 

pintos는 단일 스레드만 고려하기 때문에 thread_create()로 새로운 스레드를 생성하여, tid 변수에 저장한다. 

thread_create() 함수는 file_name을 이름으로 하며, DEFAULT 우선 순위를 부여하고, initd 함수를 실행하여 생성된 tid를 반환한다.

 

그렇다면, initd 라는 함수를 확인해보자. 

 

void initd (void *f_name) (userprog/process.c)

/* A thread function that launches first user process. */
static void
initd (void *f_name) {
#ifdef VM
	supplemental_page_table_init (&thread_current ()->spt);
#endif

	process_init ();  

	if (process_exec (f_name) < 0)
		PANIC("Fail to launch initd\n");
	NOT_REACHED ();
}

첫번째 프로세스를 실행한다.

+) 의문점🧐: process_init 함수를 살펴보면 동작이 현재 스레드를 가져오는 작업이 전부인데 다른 것은 필요 없을까?

=> 정글질문 슬랙에 따르면, 미리 정해진 용도가 있는 것이 아니라 initd와 fork로 생성된 프로세스 모두에 대해 공통된 동작을 구현하고 싶을 때 사용하는 함수라고 한다. 실제로, fork를 구현할 때 하나의 process는 하나의 thread를 갖는 다는 이유로 혼동해서 사용하는 경우가 종종있었는데 주어진 skeleton 코드에 기능을 구분하여 구현해도 좋았을 것 같다. 

(2023.06.13 update)

 

int process_exec(void *f_name) (userprog/process.c)

/* Switch the current execution context to the f_name.
 * Returns -1 on fail. */
/**
 * Parse file_name
 * Save tokens on user stack of new process.
*/
int
process_exec (void *f_name) {
	char *file_name = f_name;
	bool success;

	/* We cannot use the intr_frame in the thread structure.
	 * This is because when current thread rescheduled,
	 * it stores the execution information to the member. */
	struct intr_frame _if;
	_if.ds = _if.es = _if.ss = SEL_UDSEG;
	_if.cs = SEL_UCSEG;
	_if.eflags = FLAG_IF | FLAG_MBS;

	/* We first kill the current context */
	process_cleanup ();
	
	success = load (file_name, &_if);

	/* If load failed, quit. */
	palloc_free_page (file_name);
	if (!success)
		return -1;

	void **rspp = &_if.rsp;
	argument_stack(argc, argv, rspp);
	_if.R.rdi = argc;
	_if.R.rsi = (uint64_t)*rspp + sizeof(void *);

	hex_dump(_if.rsp, _if.rsp, USER_STACK - (uint64_t)*rspp, true);

	/* Start switched process. */
	do_iret (&_if);
	NOT_REACHED ();
}

 

구조체로 선언된 intr_frame은 context switching을 위한 정보를 갖는다.

 

process_cleanup() 실행한다.

현재 프로세스의 context를 지운다. 현재 실행 중인 스레드의 page directory, switch information을 내려준다.

새로 생성되는 process를 실행하기 위해 CPU를 점유해야하고, kernel 모드로 돌아가고 있지만 CPU를 선점하기 위해 지금 실행 중인 스레드와 context switching 하기 위한 준비이다.

 

load() 함수에서 file_name을 명령과 인자로 파싱하는 작업을 한다.

성공하면, do_iret()와 NOT_REACHED()를 통해 생성된 프로세스로 context switching한다.


pintos OS에서는 스레드가 중요한 역할을 하는 것 같다. 

하지만 아직 스레드의 개념, 그 이상으로 이해하기에는 어렵다. 여러 코드를 살펴보면서 차분히 이해해봐야겠다.

반응형