개발👩‍💻/시스템프로그래밍

프로세스 다루기 1(10-3)

gigibean 2021. 6. 5. 22:31
728x90
int execl(cont char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execv(cont char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

exec 계열의 시스템 콜에 대해 알아보자

 

사용 목적은 현재 프로세스의 코드부분을 다른 실행파일로 교체하기위함이다.

 

각각의 시스템콜들의 대한 사용방법을 설명하기 전에 exec이 무엇인지 보자

하드디스크에 있는 프로그램이 메인메모리에 적재되어 실행되고 있다.

프로세스에 관련된 속성 정보들이 프로세스 테이블에 기록되어 있다.

 

회색프로그램을 연두색 프로그램으로 교체하려고 한다.

메인메모리에 올라와 있는 프로그램 부분만 교체를 하게되고 프로세스 테이블에 있는 속성정보들은 유지가 된다. 새로운 프로세스가 만들어진다는 것은 프로세스 테이블에 엔트리를 하나 만들어서 프로세스에 대한 정보 기록하는 것인데, exec를 불렀을 때 프로세스 테이블의 정보는 바뀌지 않으므로 새로운 프로세스가 만들어진것은 아니고 기존의 프로세스는 그대로 있는데 실행되고있는 프로그램 부분을 다른 부분으로 교체한 것이다. 실행되고 있는 실행파일을 교체하고자 할때사용한다.

 

exec("ls") 라고 하게되면 기존 프로세스에서 ls를 불러와서 기존 프로그램 대신에 ls라는 프로그램 읽어와서 교체를 한다.

 

그리고 포크를 하고 나서 exec하는 경우가 많은데,

연두색 프로그램이 돌아가고 있고 이에 관련된 정보 프로세스 테이블에 기록되어 있다.

새로운 프로세스 만들어졌기 때문에 자식 프로세스 정보를 테이블에 적어둘 것이고, 부모와 같은 것이다.

 

fork에 의해 자식 프로세스를 생성한 후에 자식프로세스가 일반적으로 exec를 호출한다.

다른 프로그램으로 교체를 시킨다. exec를 부르면 하드디스크에서 해당 프로그램을 찾아 자식프로세스의 프로그램을 교체시키게 된다.

 

그래서 결과적으로 포크후 exec하고나면 부모프로세스는 자식의 기존 프로그램을 실행시키고, 자식 프로세스는 exec으로 호출했던 프로그램을 실행시키게 된다.

 

exec하게 되면 기존에 돌아가는 프로그램이 다른 프로그램으로 바뀌게 되므로 기존 프로그램을 반환할 수 없을 것이다. 그래서 exec호출에 성공하는 경우에는 반환값을 받을 수 없다. 하지만 실패할 경우 기존프로그램이 남아 돌아가고 있는 것이니까 -1을 리턴한다. 

.exec을 실행했지만 반환값이 리턴됐을 때는 exec에 실패한 것이다.

 

exec계열의 함수가 4개 있는데 함수의 이름이 조금씩 다르다. 

execl, execv, execlp, execvp 

뒤에 l이 붙어있는 계열의 함수와 v가 붙어있는 계열의 함수가 있다.

l은 list 를 의미하고, v는 vector로 배열을 의미한다.

 

exec은 기본적으로 새로운 프로그램으로 바꾸고자하는 프로그램의 이름을 인자로 넘겨준다.

그리고 더하여 옵션을 줄수도 있고 안줄 수도 있다.

exec("ls", ..)

옵션을 어떻게 주느냐에 따라 옵션을 리스트로, 배열형태로 줄 것이라는 것을 의미한다.

커맨트라인 인자들을 주는 방법에 차이가 있다.

뒤에 p의 유무에 상관없이 인자를 주는 방법은 같다

l계열 같은 경우,첫인자에 exec하고자하는 프로그램의 이름을 넣어준다.

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...); 

그런데 execl은 path로 되어 있고, execlp는 file이라고 되어 있다.

이 차이점은 p의 유무이다.

execl은 실행파일의 이름을 적는다. 

두번째부터는 커맨드라인에서 주어지는 인자들을 나열하는 것이다.

여기서는 리스트형식으로 나열하기 때문에 콤마로 나눠서 나열한다.

execl같은 경우 execl("ls", "ls", "-l", "-a", "abc", NULL); 이런식으로 argv인자들을 콤마로 구분하여 넣어준다.

이렇게 나열을 하는데 어디서 끝난다는 표시를 하기 위해서 더이상 없다는 것을 표시해주기 위해 마지막애 NULL을 넣어준다.

execl에서 두번째 인자에 꼭 argv[0]을 넣어주어야 하기 때문에 추가적으로 주어지는 인자가 없더라도 

execl("ls", "ls", NULL) 이라고 해주어야한다. 더하여 뒤에 argv[0] 만왔더라도 NULL은 꼭 표기해주어야한다. 

이렇게 인자의 개수가 가변적인 함수도 가능하고 이런 함수를 가변인자 함수라고 한다.

가변인자 함수를 정의할 수 있는데 끝에는 꼭 마지막이라는 마크가 필요하다.

이러한 가변인자함수의 예로 printf가 있다.

printf("hello");

printf("a%d", a);

가변인자를 가진함수는 특별한 마크가 필요하다고 했는데 왜 없냐면, %로 구분되는 것을 보고 뒤에 인자가 몇개가 나오는지 알 수 있기 때문에 따로 마크가 필요 없게 된다. 

 

execv 는 배열에 담아서 제공한다.

메인 함수를 정의할 때 main(int argc, char* argv[]) 

이와 같은 형식의 인자로 넘겨주면 된다.

 

뒤에 p의 유뮤

p가 없는 것은 교체할 프로그램을 적어주는 첫인자에다가 교체할 프로그램의 절대 혹은 상대 경로를 적어주어야 한다.

앞의 예시로 execl("ls",,,)라고 예시를 들긴 했지만 사실 저렇게 하면 못찼는다.

ls라는 프로그램이 있는 위치를 정확히 적어주어야 한다.

execl("/bin/ls",..) 라는 식으로 경로를 정해주어야 하며 만약 앞의 예시처럼 execl("ls",..)라고 하게되면 ./ls 라고 인식이되어 해당 경로에서 ls를 찾게 된다.

 

이와 같이 execl, execv는 첫인자로써 교체하고자하는 프로그램의 경로를 적어줄 때 정확한 경로를 적어주어야 한다.

p가 있는 것은 execlp, execvp 정확한 위치가 아니라 execlp("ls",..)라고 한다면 환경변수 PATH에 지정된 디렉터리에서 실행 파일을 찾는다. 실행파일을 찾는 위치를 지정해놓는다. 지정된 디렉토리에 가서 찾고 없으면 실패하게 된다. 교체하고자하는 이름을 적어주게 되면 프로그램이 위치하고 있는 디렉토리가 정해져있고 그곳에서 찾아서 발견하면 리턴하게된다.

이 디렉토리를 지정해 놓은 곳이 PATH라는 환경변수이다. 

PATH="/bin: /usr/bin: /usr/local/bin"

이렇게 지정을 해주면 우선 첫번째 /bin에서 찾고 발견되면 이것으로 실행시킨다.

없다면 다음디렉토리인 /usr/bin에서 찾고 있다면 실행시킨다..

이런식으로 순서대로 첫 인자로 주어진 프로그램 찾아서 exec을 시킨다.

 

PATH라는 환경변수에 값을 보기 위해서 prinenv 라는 명렁어를 사용하면 된다. 

환경변수와 값이 각각 있다.

그중에서 PATH라는 환경변수에서 찾아서 리턴하는 것이다.

 

execlp는 커멘드라인 인자들을 리스트 형식으로, 그리고 구체적인 경로가 아닌 PATH에서 찾도록 하는 명령어를 보자면

$ ls -l apple
...
execlp("ls", "ls", "-l", "apple", NULL);

exec()이 성공적으로 호출되면 프로그램이 교체되고 교체된 프로그램은 시작부분부터, 처음부터 시작된다.

포크를 하게 되면 새로만들어진 자식프로세스는 포크로 부터 리턴한 이후부터이지만, exec으로 전혀다른 프로그램으로 교체하게되면 교체한 프로그램의 시작, main부터 시작하게된다.

포크는 새로운 아이를 만들어지는 것이지만 exec은 있는 프로세스 그대로 두고 실행되는 프로그래밍 내용만 교체 되는 것이기 때문에 새로운 프로세스 만들어지는 게 아니니 pid는 그대로, 속성정보도 그대로 바뀌지 않는다.

 

int main()
{
	printf("before executing ls -l\n");
	execl("/bin/ls", "ls", "-l", NULL);
	printf("after executing ls -l\n");
} 
$ ./ex07-03
before executing ls -l
-rwxr-xr-x 1 usp student 13707 Oct 24 21:57 ex07-03 

exec을 하기 전에 before~이 출력이 될 것이다. 

ls라는 프로그래밍으로 exec하게 된다. 다시말해 실행되고 있는 프로그램에서 exec이 실행되면 ls프로그램으로 교체되고, ls의 처음 시작부분부터 실행하게 된다. 성공적으로 exec된 경우, 시스템콜로부터 리턴하지 않으니, after~는 출력되지 않을 것이다.

 

프로그램이실행되는 도중에 ls프로그램으로 교체되어 ls 실행결과가 화면에 출력된다.

 

똑같은 프로그램을 execv로 프로그래밍해보면

int main()
{
	char *arg[] = {"ls", "-l", NULL};
	printf("before executing ls -l\n");
	execv("/bin/ls", arg);
	printf("after executing ls -l\n");
}
$ ./ex07-04
before executing ls -l
-rwxr-xr-x 1 usp student 13707 Oct 24 21:57 ex07-04

배열형태로 주기위해서 앞에서 설명한 것처럼 캐릭터 포인터에 배열형태로 두번째 인자를 주게된다.

arg라는 배열은 아래 그림과 같은 형태로 되어 있다.

마지막에 NULL이 나오면 중단된다. 그렇기 때문에 마지막은 NULL을 넣어준다.

이것을 execv 두번째 인자에 넣어준다.

 

#include <unistd.h>
#include <stdio.h>
int main(int argc, char *argv[])
{
	execv("/bin/cat", argv);
	fprintf(stderr, "exec failure...\n");
}
$ ./a.out hello.txt

위 파일을 컴파일해서 아래와 같이 실행시키며 인자를 주게되면 cat hello.txt한것과 같은 결과가 출력된다.

// ex07-05.c
int main(int argc, char *argv[])
{
	int i;
	for (i = 0; i < argc; i++)
	printf(“[%d] %s\n”, i, argv[i]);
}

위 코드와 같이 인자를 받아서 하나씩 출력하는 프로그램이 있고 이 프로그램을 exec하는 프로그램을 만들어서

int main()
{
	execl(“ex07-05”, “apple”, “option”, NULL);
}

이 프로그램을 컴파일해서 아래와 같이 a, b, c라는 인자를 줬을 때

$ ./a.out a b c

exec이 되서 해당 ex07-05 프로그램을 실행시키며 받은 인자는 apple, option이기 때문에 해당 인자를 출력하게 된다. (터미널에서 쓴 인자를 받는 프로세스가 아니라 다른 프로세스로 exec되었기 때문에)

 

int main()
{
	for (int i = 0; i < 10; i++) {
		sleep(1);
		if (i == 5) {
			printf("Now I am ...");
			execl("./batman", "batman", NULL);
		}
		else printf("I am Superman! i=%d\n", i);
	}
}
int main()
{
	for (int i = 0; i < 10; i++) {
		sleep(1);
		printf("I am Batman! i=%d\n", i);
	}
}
I am Superman! i = 0
I am Superman! i = 1
I am Superman! i = 2
I am Superman! i = 3
I am Superman! i = 4
I am Batman! i = 0
I am Batman! i = 1
I am Batman! i = 2
I am Batman! i = 3
...

위 수퍼맨이라는 프로그램을 실행시키면 위와 같은 결과가 나온다.

우선 Now I am 이 나오지 않았고,

밑에있는 프로그램으로 바뀌었다. 그래서 배트맨으로 출력이 된다.

 

여기서 now i am  뒤에 \n 을 넣어 다시 컴파일 해서 실행해보면 

Now I am이 나온다.

어렇게 된 이유는 printf 는 표준 출력으로 출력하는데,

앞서 배운 것처럼 이 라이브러리 사용하는 I/O는 버퍼를 사용한다.

그래서 Now I am을 버퍼에 넣어 놓았다가 하나의 라인이 완성이 되면 터미널에 출력하게 된다.

(표준 출력 I/O는 라인단위로 버퍼링한다.)

그래서 \n이 오게 되면 라인이 완성이 되며 출력되게 된다.

 

반응형