사용자 프로그램의 프로그램 실행, 메모리 할당, 파일 접근과 같은 가상 머신과 관련된 기능들을 운영체제에게 요청할 수 있도록, 운영체제는 사용자에게 API를 제공한다.
보통 운영체제는 응용 프로그램이 사용 가능한 수백 개의 시스템 콜을 제공한다.
운영체제가 프로그램 실행, 메모리와 장치에 접근, 기타 이와 관련된 여러 작업을 진행하기 위해 이러한 시스템 콜을 제공하기 때문에, 우리는 운영체제가 표준 라이브러리(standard library)를 제공한다고 일컫기도 한다.
프로세스 API를 왜 사용하는가?
fork(): 자식 프로세스를 생성, 이때 생성된 프로세스는 호출한 프로세스의 복사본이고, 새 프로세스를 위한 메모리가 할당된다.
p1.c
// p1.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n ", (int)getpid());
int rc = fork(); // 자식 프로세스 생성
if (rc < 0)
{ // fork 실패; 종료
fprintf(stderr, " fork failed\n ");
exit(1);
}
else if (rc == 0)
{ // 자식 (새 프로세스)
printf(" hello, I am child(pid: % d)\n ", (int)getpid());
}
else
{ // 부모 프로세스는 이 경로를 따라 실행한다 (main)
printf(" hello, I am parent of % d(pid: % d)\n ", rc, (int)getpid());
}
return 0;
}
실행결과
위의 프로그램 p1.c
는 실행을 한 후 자식 프로세스를 fork()로 생성한다.
이때 생성된 프로세스틑 호출한 프로세스의 복사본이고, 해당 프로세스를 위한 메모리가 새로 할당된다.
특이한 점은, 자식 프로세스는 main() 함수의 첫 부분부터 시작하지 않는다는 것이다.
(hello world가 한번밖에 실행되지 않았음!)
그리고 한가지 더 알아야 할 점은, 이 프로그램은 부모 프로세스와 자식 프로세스의 실행 순서를 보장할 수 없다는 것이다. 즉 자식 프로세스가 먼저 실행되고 그 다음에 부모 프로세스가 실행될 수도 있다.
이는 CPU 스케줄러가 실행할 프로세스를 선택할 때 비결정성 문제가 발생하기 때문. 이는 병행성 부분에서 더 다룰 예정이다.
wait(): 자식 프로세스가 끝날 때까지 부모 프로세스는 대기, 자식 프로세스가 끝나면 자식 프로세스의 pid를 리턴한다.
p2.c
// p2.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
printf(" hello world (pid:%d)\n ", (int)getpid());
int rc = fork();
if (rc < 0)
{ // fork 실패; 종료
fprintf(stderr, " fork failed\n ");
exit(1);
}
else if (rc == 0)
{ // 자식 (새 프로세스)
printf(" hello, I am child (pid:%d)\n ", (int)getpid());
}
else
{ // 부모 프로세스는 이 경로를 따라 실행한다 (main)
int wc = wait(NULL); // 자식 프로세스가 종료될 때까지 대기
printf(" hello, I am parent of %d (wc:%d) (pid:%d)\n ", rc, wc, (int)getpid());
}
return 0;
}
위의 프로그램 p2.c
는 실행을 한 후 자식 프로세스를 fork()로 생성한다.
이때 생성된 프로세스틑 호출한 프로세스의 복사본이고, 해당 프로세스를 위한 메모리가 새로 할당된다.
이후 부모 프로세스는 wait()을 마주하게 되는데,
즉, 이 프로그램 p2.c
는 항상 자식 프로세스가 먼저 실행이 되는 것이다.
exec(): 자기 자신이 아닌 다른 프로그램을 실행해야 할 때 사용. 다른 프로세스를 실행시킬 때, 현재 프로세스의 메모리 공간을 덮어쓴다.
a.out
이라는 프로그램에서 exec()
이 호출되면, 해당 메모리에 새롭게 부른 /bin/ls
프로세스가 덮어씌워지는 것이다.execl
,execlp
,execle
, execv
,execvp
,execve
p3.c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>
int main(int argc, char *argv[])
{
printf("hello world (pid:%d)\n ", (int)getpid());
int rc = fork();
if (rc < 0)
{ // fork 실패함; exit
fprintf(stderr, "fork failed\n ");
exit(1);
}
else if (rc == 0)
{ // 자식 (새 프로세스)
printf("hello, I am child(pid: % d)\n ", (int)getpid());
char *myargs[3];
myargs[0] = strdup("wc"); // 프로그램:"wc"(단어 세기)
myargs[1] = strdup("p3.c"); // 인자: 단어 셀 파일
myargs[2] = NULL; // 배열의 끝 표시
execvp(myargs[0], myargs); //"wc"실행
printf("this shouldn't print out");
}
else
{ // 부모 프로세스는 이 경로를 따라 실행한다 (main)
int wc = wait(NULL);
printf("hello, I am parent of % d(wc: % d)(pid: % d)\n ", rc, wc, (int)getpid());
}
return 0;
}
위의 프로그램은 먼저 부모 프로세스가 main()을 따라 실행되고, 자식 프로세스를 fork()한다.
이후 wait()을 만나 자식 프로세스가 완료될 때까지 기다린다.
fork된 자식 프로세스는 일단은 부모 프로세스의 복제품이다. 그러나 여기서 차이점은, execvp()를 통해 부모 프로세스와는 전혀 상관없는 프로그램을 새로 실행시킨다는 것이다.
execvp(myargs[0], myargs);
을 보면 wc(word count)
명령어에 myargs(=p3.c)
의 인자를 넣고 실행시킨다.
실행결과
printf("this shouldn't print out");
가 실행되지 않는 것이다.execvp
가 성공적으로 수행되어 자식 프로세스(pid=43423)에 있는 메모리를 wc
가 덮어써버렸기 때문이다.왜 사용? 그래야만 Unix 쉘을 구현할 수 있다
예시: wc p3.c > newfile.txt
wc
실행, 이후 newfile.txt에 출력등등
fork() 를 호출하는 프로그램 작성.
fork 호출 전에 x=100, 자식 프로세스에서는 그 변수 무엇? 만약 자식이 값 변경하면?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int x = 100;
printf("hello world (pid:%d)\n ", (int)getpid());
int rc = fork(); // 자식 프로세스 생성
if (rc < 0)
{ // fork 실패; 종료
fprintf(stderr, "fork failed\n ");
exit(1);
}
else if (rc == 0)
{ // 자식 (새 프로세스)
printf(" hello, I am child(pid: % d)\n ", (int)getpid());
printf(" child x = %d\n ", x);
x = 200;
printf(" child changed x = %d\n", x);
}
else
{ // 부모 프로세스는 이 경로를 따라 실행한다 (main)
wait(NULL);
printf("hello, I am parent of % d(pid: % d)\n ", rc, (int)getpid());
printf("parent x = %d\n", x);
}
return 0;
}
open() 시스템 콜 이용하여 파일 여는 프로그램 작성, 새 프로세스를 위해 fork() 호출할 것.
자식과 부모가 open()에 의해 반환된 파일 디스크립터에 접근 가능?
부모와 자식이 동시에 파일 쓰기 가능?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/wait.h>
int main()
{
char *file_path = "example.txt";
int file_descriptor;
// 부모 프로세스에서 파일 열기
file_descriptor = open(file_path, O_WRONLY | O_CREAT, 0644);
// fork()를 호출하여 자식 프로세스 생성
pid_t pid = fork();
if (pid == 0)
{
// 자식 프로세스
// 자식 프로세스에서 파일에 쓰기 작업 수행
char *child_message = "Child\n";
write(file_descriptor, child_message, 7);
close(file_descriptor);
exit(0);
}
else if (pid > 0)
{
// 부모 프로세스
// 부모 프로세스에서 파일에 쓰기 작업 수행
char *parent_message = "Parent\n";
write(file_descriptor, parent_message, 8);
// wait(NULL); // 여기에 넣어도 파일 잘 작성됨
close(file_descriptor);
wait(NULL); // 자식 프로세스의 종료를 기다림
}
else
{
// fork() 실패
fprintf(stderr, "Fork failed\n");
return 1;
}
return 0;
}
자식은 "hello" 부모는 "goodbye" 출력하는데, 항상 자식이 먼저 출력하게끔 하는 방법은? (wait() 없이)
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
int pipefd[2];
if (pipe(pipefd) == -1)
{
perror("pipe");
return 1;
}
pid_t pid = fork();
if (pid == 0)
{
// 자식 프로세스
close(pipefd[0]); // 자식은 읽기용 파이프 닫기
write(pipefd[1], "hello", 6); // 쓰기용 파이프에 hello 쓰기
close(pipefd[1]); // 자식 쓰기용 파이프 닫기
exit(0);
}
else if (pid > 0)
{
// 부모 프로세스
close(pipefd[1]); // 부모는 쓰기용 파이프 닫기
char buffer[6];
read(pipefd[0], buffer, 6); // 읽기용 파이프에서 읽어오고, 그 값을 buffer에 넣어줌. 읽을 것 없으면 대기.
printf("%s\n", buffer); // 읽어온 버퍼 출력
close(pipefd[0]); // 부모의 읽기용 파이프 닫기
printf("goodbye\n");
}
else
{
// fork() 실패
fprintf(stderr, "Fork failed\n");
return 1;
}
return 0;
}
exec() 계열의 함수 호출. 여러 변형이 있는 이유는?
// execl(): l이 붙으면 리스트 형태로 연달아서 명령어 및 옵션 넣음,
// 프로그램이 들어있는 디렉토리 명까지 넣어줘야함
execl("/bin/ls", "ls", "-l", NULL);
// execle(): e가 붙으면 환경변수 지정 가능
char *envp[] = {"PATH=/bin", NULL};
execle("/bin/ls", "ls", "-l", NULL, envp);
// execlp(): p가 붙으면 현재 경로에서 명령 실행, 디렉토리명 필요 없음
execlp("ls", "ls", "-l", NULL);
// execv(): v 붙으면 인수를 배열로 넣음,
// 프로그램이 들어있는 디렉토리 명까지 넣어줘야함
char *argv[] = {"ls", "-l", NULL};
execv("/bin/ls", argv);
// execvp(): p가 붙으면 현재 경로에서 명령 실행, 디렉토리명 필요 없음
char *argv[] = {"ls", "-l", NULL};
execvp("ls", argv);
// execve(): e가 붙으면 환경변수 지정 가능
char *argv[] = {"ls", "-l", NULL};
char *envp[] = {"PATH=/bin", NULL};
execve("/bin/ls", argv, envp);
wait()가 반환하는 것은 무엇?
자식 프로세스가 wait을 호출하면?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
int c = wait(NULL);
printf("%d\n", c);
// 자식 프로세스
printf("child\n");
}
else if (pid > 0)
{
int p = wait(NULL);
printf("%d\n", p);
// 부모 프로세스
printf("parent\n");
}
else
{
// fork() 실패
fprintf(stderr, "Fork failed\n");
return 1;
}
return 0;
}
wait() 대신 waitpid() 사용하면 좋은점?
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// 자식 프로세스
printf("child\n");
}
else if (pid > 0)
{
int p = waitpid(pid, NULL, 0);
printf("%d\n", p);
// 부모 프로세스
printf("parent\n");
}
else
{
// fork() 실패
fprintf(stderr, "Fork failed\n");
return 1;
}
return 0;
}
waitpid(child_pid, status, options)
는 wait() 보다 더 다양한 기능이 있음. 좀 더 정교하게 자식 프로세스의 변화에 따른 제어가 가능자식 프로세스에서 표준 출력을 닫으면 어떻게 되는가?
자식 프로세스에서 표준 출력을 닫으면, 자식 프로세스에서는 printf 해도 아무것도 안 나옴
근데 자식 프로세스에서 표준 출력을 닫았다고 해서 부모 프로세스에 영향을 주진 않음. 부모 프로세스에서는 출력 잘 됨.
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
pid_t pid = fork();
if (pid == 0)
{
// Child process
close(STDOUT_FILENO); // Close STDOUT_FILENO
printf("This will not be printed\n"); // Attempt to print, but no output will occur
return 0;
}
else if (pid > 0)
{
// Parent process
// Wait for the child process to finish
wait(NULL);
printf("Child process completed\n");
}
else
{
// Fork failed
printf("Failed to create child process\n");
}
return 0;
}
실행 결과
두 개의 자식 프로세스 만들고, pipe() 시스템 콜을 사용하여 한 자식의 표준 출력을 다른 자식의 입력으로 연결하는 프로그램 작성
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main()
{
int pipefd[2];
pid_t child1, child2;
// Create pipe
if (pipe(pipefd) == -1)
{
perror("pipe");
return 1;
}
// Create first child process
// 쓰기용 자식 프로세스
child1 = fork();
if (child1 == 0)
{
// Child 1 process
close(pipefd[0]); // 읽기용 파이프 닫기
// Redirect stdout to the write end of the pipe
dup2(pipefd[1], STDOUT_FILENO); // 이 자식의 표준출력을 쓰기용 파이프로 연결
close(pipefd[1]); // 이후 쓰기용 파이프 닫기
// Execute the desired command
execlp("ls", "ls", "-l", NULL); // ls -l 실행 -> 표준 출력으로 안 가고 출력파이프로 감
perror("execlp");
return 1;
}
else if (child1 == -1)
{
perror("fork");
return 1;
}
// Create second child process
// 읽기용 자식 프로세스
child2 = fork();
if (child2 == 0)
{
// Child 2 process
close(pipefd[1]); // 쓰기용 파이프 닫기
// Redirect stdin to the read end of the pipe
dup2(pipefd[0], STDIN_FILENO); // 이 자식의 표준입력을 읽기용 파이프로 연결
close(pipefd[0]); // 이후 읽기용 파이프 닫기
// Execute the desired command (e.g., read from stdin)
execlp("grep", "grep", "example", NULL); // 입력파이프에서 값을 읽어옴 -> 이 입력을 가지고 grep 실행
perror("execlp");
return 1;
}
else if (child2 == -1)
{
perror("fork");
return 1;
}
// Close both ends of the pipe in the parent process
close(pipefd[0]);
close(pipefd[1]);
// Wait for both child processes to finish
waitpid(child1, NULL, 0);
waitpid(child2, NULL, 0);
return 0;
}