12 분 소요


프로그램 실행

  • fork() 후
    • 프로세스 이미지가 변경하는 것은 아니기 때문에 자식 프로세스는 부모 프로세스와 똑같은 코드 실행
  • 자식 프로세스에게 새 프로그램 실행
    • exec() 시스템 호출 사용
    • 프로세스 내의 프로그램을 새 프로그램으로 대치 (원래는 부모 프로그램이 깔려있었다면)
    • PID는 변경되지 않음
      • 단지 프로세스 내에 탑재되는 프로그램만 교체가 되는 것이다.
  • 보통 fork() 후에 exec( ) 호출
    • When a process calls one of the exec functions, the process is completely replaced by the new program.
    • New program starts executing at its main() function.
    • exec() merely replaces the current process(text, data) with a brand new program from disk.


fork/exec

  • 보통 fork() 호출 후에 exec() 호출
    • 새로 실행할 프로그램에 대한 정보를 arguments로 전달한다
  • exec() 호출이 성공하면
    • 자식 프로세스는 새로운 프로그램을 실행하게 되고
    • 부모는 계속해서 다음 코드를 실행하게 된다.
if ((pid = fork()) == 0 ){
 exec( arguments );
 exit(1);
}
// 부모 계속 실행


프로그램 실행: exec()

  • 프로세스가 exec() 호출을 하면,
    • 그 프로세스 내의 프로그램은 완전히 새로운 프로그램으로 대치
    • 자기대치(自己代置)
    • 새 프로그램의 main()부터 실행이 시작된다.

image


  • exec() 호출이 성공하면 리턴할 곳이 없어진다.
    • 부모 프로세스와의 connection이 끊어졌기 때문에 리턴값을 받을 수 없음
  • 성공한 exec() 호출은 절대 리턴하지 않는다.
    • 실패한 경우에만 리턴함. (새로운 프로그램을 실행하지 않았기 때문)
#include <unistd.h>
int execl(char* path, char* arg0, char* arg1, ... , char* argn, NULL)
int execv(char* path, char* argv[])
int execlp(char* file, char* arg0, char* arg1, ... , char* argn, NULL)
int execvp(char* file, char* argv[])

path: absolute path

file: current working directory 기준으로 한 파일

호출한 프로세스의 코드, 데이터, 힙, 스택 등을 path가 나타내는 새로운 프로그램으로 대치한 후 새 프로그램을 실행한다. 성공한 exec( ) 호출은 리턴하지 않으며 실패하면 -1을 리턴한다.


exec()

  • pathname(filename) argument
    • pathname(filename) of a file to be executed.
    • execl, execv, execle, execve take a pathname argument.
    • execlp, execvp take a filename argument.
      • If filename contains a ‘/’, it is taken as a pathname.
      • Otherwise, the executable file is searched for in the directories specified by PATH. E.g. PATH=/bin:/usr/bin:/usr/local/bin/:.
  • Argument list
    • arg0, arg1, …, argn in the execl, execlp, and execle.
    • The list of arguments is terminated by a NULL pointer. (인자의 끝을 알리기 위한 용도)
      • execl(“/bin/ls”, “ls”, -“al”, 0);
  • Argument vector
    • list와 사실상 동일하나 넘겨주는 방식의 차이가 존재
    • *argv[] in execv, execve and execvp.
    • The array of pointers is terminated by a NULL pointer.
      • char *argv[3] = {“ls”, -“al”, 0};
      • execv(“/bin/ls”, argv);
  • Environment variable
    • *envp[] in execve and execle
    • A pointer to an array of pointers to the environment strings.
    • The other functions use the environ variable.
      • char *env[2] = {“TERM=vt100”, 0};
      • execle(““/bin/ls”, “ls”, 0, env);


  • Differences among the six exec functions(family)

image

경로 이름으로 전달 vs. 파일 이름으로 전달


  • execve is a system call.
    • execl, execv, execle, execlp, execvp are library functions.
    • Relationship of the six exec functions.

image

#include <unistd.h>

int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv []);
int execle(const char *pathname, const char *arg0,  /* (char *)0,
 char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp []);
int execlp(const char *filename, const char *arg0, ... /* (char *)0 */ );
int execvp(const char *filename, char *const argv []);
 			All six return: -1 on error, no return on success 


execute1.c

#include <stdio.h>
/* 자식 프로세스를 생성하여 echo 명령어를 실행한다. */
int main( )
{
    printf("부모 프로세스 시작\n");
    if (fork( ) == 0) {
        execl("/bin/echo", "echo", "hello", NULL); // 자식 주소 공간 변경
        fprintf(stderr,"첫 번째 실패"); // exec 성공 시 실행 안됨
        exit(1); // exec 성공 시 실행 안됨
    }
    printf("부모 프로세스 끝\n");
} 
$ execute1
부모 프로세스 시작
hello
부모 프로세스 끝

exec()이 성공했단 의미는 자식 프로세스의 공간이 echo 프로그램(echo.c)으로 대치되었기 때문에 그 아래 문장은 모두 없어진다는 뜻이다.


execute2.c

#include <stdio.h> …
/* 세 개의 자식 프로세스를 생성하여 각각
다른 명령어를 실행한다. */
int main( )
{
    printf("부모 프로세스 시작\n");
    if (fork( ) == 0) {
        execl("/bin/echo", "echo", "hello", NULL);
        fprintf(stderr,"첫 번째 실패");
        exit(1);
    }
    if (fork( ) == 0) {
        execl("/bin/date", "date", NULL); // argument 없음
        fprintf(stderr,"두 번째 실패");
        exit(2);
    }
    if (fork( ) == 0) {
        execl("/bin/ls","ls", "-l", NULL); // argument 1개
        fprintf(stderr,"세 번째 실패");
        exit(3);
    }
    printf("부모 프로세스 끝\n");
}
$ execute2
부모 프로세스 시작
부모 프로세스 끝
hello
2014년 4월 22일 화요일 오후 08시 43분 47초
총 50
-rwxr-xr-x 1 chang faculty 24296 4월 16일 13:37 execute2
-rw-r--r-- 1 chang faculty 556 4월 14일 13:17 execute2.c


execute3.c

#include <stdio.h> …
/* 명령줄 인수로 받은 명령을 실행시킨다. */

int main(int argc, char *argv[])
{
    int child, pid, status;
    pid = fork( );
    
    if (pid == 0) { // 자식 프로세스
        execvp(argv[1], &argv[1]); // 부모 프로세스의 argument을 다시 전달
        fprintf(stderr, "%s:실행 불가\n",argv[1]); 
    } else { // 부모 프로세스 
        child = wait(&status);
        printf("[%d] 자식 프로세스 %d 종료 \n", getpid(), pid);
        printf("\t종료 코드 %d \n", status>>8);
    }
}
$ execute3 wc you.txt
25 68 556 you.txt
[26470] 자식 프로세스 26471 종료
종료 코드 0

image

자식 프로세스의 종료는 exec() 호출이 되면 그 안에서 되는 것임


exec()

  • Example
#include "apue.h"
#include <sys/wait.h>
char *env_init[] = { "USER=unknown", "PATH=/tmp", NULL };
int main(void)
{
    pid_t pid;
    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) { /* specify pathname, specify environment */
        if (execle("/home/sar/bin/echoall", "echoall", "myarg1",
                   "MY ARG2", (char *)0, env_init) < 0)
            err_sys("execle error");
    }
    if (waitpid(pid, NULL, 0) < 0)
        err_sys("wait error");
    if ((pid = fork()) < 0) {
        err_sys("fork error");
    } else if (pid == 0) { /* specify filename, inherit environment */
        if (execlp("echoall", "echoall", "only 1 arg", (char *)0) < 0)//환경변수로 찾아감
            err_sys("execlp error");
    }
    exit(0);
}


  • 자식이 실행하는 echoall 프로그램
#include "apue.h"
int main(int argc, char *argv[])
{
    int i;
    char **ptr;
    extern char **environ;
    for (i = 0; i < argc; i++) /* echo all command-line args */
        printf("argv[%d]: %s\n", i, argv[i]);
    
    for (ptr = environ; *ptr != 0; ptr++) /* and all env strings */
        printf("%s\n", *ptr);
    exit(0);
}
$ ./a.out
argv[0]: echoall
argv[1]: myarg1
argv[2]: MY ARG2
USER=unknown
PATH=/tmp
$ argv[0]: echoall
argv[1]: only 1 arg
USER=sar
LOGNAME=sar
SHELL=/bin/bash
…
HOME=/home/sar


system()

  • fork()와 wait()을 합친 시스템 콜
  • Both ANSI C and POSIX define an interface that couples spawning a new process
  • and waiting for its termination
    • think of it as synchronous process creation.
  • If a process is spawning a child only to immediately wait for its termination, it makes sense to use this interface
    • 바로 wait하는 경우 굳이 생성(fork)과 wait를 분리할 필요가 없어짐 -> 하나의 system()으로 대체
  • It is common to use system( ) to run a simple utility or shell script, often with the explicit goal of simply obtaining its return value


#include <stdlib.h>
int system(const char *cmdstring);
//이 함수는 /bin/sh –c cmdstring를 호출하여 cmdstring에 지정된 명령어를
//실행하며, 명령어가 끝난 후 명령어의 종료코드를 반환한다.
  • 자식 프로세스를 생성하고 /bin/sh 로 하여금 지정된 명령어(cmdstring)를 실행시킨다(exec)
    • system(“date > file”);
  • system() 함수 구현
    • fork(), exec(), waitpid() 시스템 호출을 이용
  • 반환값
    • 명령어의 종료코드
    • -1 with errno: fork() 혹은 waitpid() 실패
    • 127 : exec() 실패


system() 함수 구현

#include <sys/types.h> /* system.c */
#include <sys/wait.h>
#include <errno.h>
#include <unistd.h>
int system(const char *cmdstring)
{
    pid_t pid; int status;
    if (cmdstring == NULL) /* 명령어가 NULL인 경우 */
        return(1);
    if ( (pid = fork()) < 0) {
        status = -1; /* 프로세스 생성 실패 */
    } else if (pid == 0) { /* 자식 */
        execl("/bin/sh", "sh", "-c", cmdstring, (char *)0);
        _exit(127);  /* execl 실패 */
    } else { /* 부모 */
        while (waitpid(pid, &status, 0) < 0)
            if (errno != EINTR) { 	/* 오류 원인 시스템변수 errno로 전달 */
 				status = -1; 		/* waitpid()로부터 EINTR외의 오류 */
                break; 				/* EINTR은 시스템 호출중 인터럽트에 의해 수행이 중단된 오류*/
            }               		/*수행이 중단된 오류*/
    }
    return(status);
}


Implementing system()

/* * my_system - synchronously spawns and waits for the command
* "/bin/sh -c <cmd>".
* Returns -1 on error of any sort, or the exit code from the
* launched process. Does not block or ignore any signals. */
int my_system (const char *cmd){
    int status;
    pid_t pid;
    
    pid = fork ( );
    
    if (pid == -1)
        return -1;
    else if (pid == 0) {
        const char *argv[4];
        argv[0] = "sh"; argv[1] = "-c"; argv[2] = cmd; argv[3] = NULL;
        execv ("/bin/sh", argv);
        exit (-1); // exec에 성공하면 이 줄은 실행 안 함
    }
    
    if (waitpid (pid, &status, 0) == -1)
        return -1;
    else if (WIFEXITED (status)) //정상 종료한 경우
        return WEXITSTATUS (status);
    
    return -1;
}

앞에서는 list를 통해 전달을 했다면 이 예제는 포인터를 통해 전달했다는 차이가 있다.


syscall.c

#include <sys/wait.h>
#include <stdio.h>
int main()
{
    int status;
    if ((status = system("date")) < 0)
        perror("system() 오류");
    printf("종료코드 %d\n", WEXITSTATUS(status));
    
    if ((status = system("hello")) < 0)
        perror("system() 오류");
    printf("종료코드 %d\n", WEXITSTATUS(status));
    
    if ((status = system("who; exit 44")) < 0)
        perror("system() 오류");
    printf("종료코드 %d\n", WEXITSTATUS(status));
}
$ syscall
2014. 03. 07 () 11:20:38 KST
종료코드 0
sh: hello: command not found
종료코드 127
chang tty 2014-03-18 10:58 (:0)
….
종료코드 44


9.3 입출력 재지정

입출력 재지정 (2장 리눅스 사용 참조)

  • 명령어의 표준 출력이 파일에 저장
    • $ 명령어 > 파일
  • 출력 재지정 기능 구현
    • 파일 디스크립터 fd를 표준출력(1)에 dup2()
    • fd = open(argv[1], O_CREAT O_TRUNC O_WRONLY, 0600);
      • // argv[1]이 나타내는 파일을 쓰기전용으로 열고, 기존 내용은 모두 지움.
      • // 파일이 없으면 새로 생성하고 사용권한은 0600으로 함.
    • dup2(fd, 1);
#include <unistd.h> //4장 파일 입출력 참조
int dup(int oldfd);
//oldfd에 대한 복제본인 새로운 파일 디스크립터를 생성하여 반환한다. 실패시 -1 반환
int dup2(int oldfd, int newfd);
//oldfd을 newfd에 복제하고 복제된 새로운 파일 디스크립터를 반환한다. 실패시 -1 반환


redirect1.c

#include <stdio.h>
#include <fcntl.h>

/* 표준 출력을 파일에 재지정하는 프로그램 */
int main(int argc, char* argv[])
{
    int fd, status;
    fd = open(argv[1], O_CREAT|O_TRUNC|O_WRONLY, 0600);
    dup2(fd, 1); /* 파일을 표준출력에 복제 */
    close(fd);
    printf("Hello stdout !\n"); 
    fprintf(stderr,"Hello stderr !\n"); //표준오류를 통해 출력
}
$ redirect1 out
Hello stderr !
$ cat out
Hello stdout ! 


redirect2.c

#include <stdio.h>
#include <fcntl.h>

/* 자식 프로세스의 표준 출력을 파일에 재지정한다. */
int main(int argc, char* argv[])
{
    int child, pid, fd, status;

    pid = fork( );
    if (pid == 0) {
        fd = open(argv[1],O_CREAT | O_TRUNC| O_WRONLY, 0600);
        dup2(fd, 1); // 파일을 표준출력에 복제
        close(fd);
        execvp(argv[2], &argv[2]);
        fprintf(stderr, "%s:실행 불가\n",argv[1]);
    } else {
        child = wait(&status);
        printf("[%d] 자식 프로세스 %d 종료 \n", getpid(), child);
    }
}
$ redirect2 out wc you.txt
[26882] 자식 프로세스 26883 종료
$ cat out
 25 68 556 you.txt
 줄 단어 문자수


9.4 프로세스 그룹

Process groups

  • A collection of one or more processes.
    • 프로세스 그룹은 여러 프로세스들의 집합이다
  • Usually associated with the same job.
    • when a shell starts up a pipeline (e.g., when a user enters ls more),
    • all the commands in the pipeline go into the same process group
  • image


  • Each process is owned by a user and a group
  • Each process is also part of a process group, which simply expresses its relationship to other processes, and must not be confused with the aforementioned user/group concept
    • “userid라는 유저 아이디를 가진 유저가 만든 프로세스 그룹”과 혼동 노노


프로세스 그룹

  • 보통 부모 프로세스(그룹 리더)가 생성하는 자손 프로세스들은 부모와 같은 프로세스 그룹에 속한다.
  • 프로세스 그룹은 signal 전달 등을 위해 사용됨.
    • The notion of a process group makes it easy
      • to send signals to processes in the pipeline (e.g., $ ls more)
        • multicast로 효율적으로 보내기 위함
      • to receive signals from the same terminal
      • get information on an entire pipeline
  • From the perspective of a user, a process group is closely related to a job

image


Process groups

  • Process group ID
    • Each process group has a unique PGID.
    • Each process group can have the process group leader, whose PID equals its PGID.
      • 프로세스 그룹 리더: Process GID = PID (리더의 PID)
  • The process group exists, as long as there is at least one process in the group, regardless whether the group leader terminates or not.
    • 리더가 없어도 프로세스 그룹 존재 O
$ ps -o pid,ppid,pgid,comm | cat
 PID 	PPID 	PGID 	COMMAND
27463 	27462 	27463 	bash
27554 	27463 	27554 	ps
27555 	27463 	27554 	cat
$

ps와 cat은 하나의 job을 형성하니까 동일 process group id를 갖고 shell(bash)는 별도의 process


프로세스 그룹

  • 프로세스 IDs
    • 프로세스 ID(PID)
    • 프로세스 그룹 ID(GID)
    • 각 프로세스는 하나의 프로세스 그룹에 속함.
    • 각 프로세스는 자신이 속한 프로세스 그룹 ID를 가지며 fork() 시 물려받는다.
#include <sys/types.h>
#include <unistd.h>
pid_t getpgrp(void);
// 호출한 프로세스의 프로세스 그룹 ID를 반환한다.
#include <unistd.h>

pid_t getpgid(pid_t pid);
 	Returns: process group ID if OK, -1 on error 
  • Return the process group ID of the process with pid.

    • 주어진 pid가 속한 프로세스 그룹의 PGID

    • If pid is 0, return the process group ID of the calling process.

      • getpgid(0); is equivalent to getpgrp();


프로세스 그룹: pgrp1.c

#include <sys/types.h>
#include <unistd.h>
main()
{
    int pid, gid;
    printf("PARENT: PID = %d GID = %d\n", getpid(), getpgrp());
    pid = fork();
    if (pid == 0) { // 자식 프로세스
        printf("CHILD: PID = %d GID = %d\n", getpid(), getpgrp());
    }
}
$ pgrp1
PARENT: PID = 17768 GID = 17768
CHILD: PID = 17769 GID = 17768

parent process와 child process는 동일 process group에 속한다.

parent는 프로세스 그룹의 리더이지만

child는 리더는 아니다 (PID와 GID가 다르기 때문)


프로세스 그룹

  • 프로세스 그룹 만들기
    • A process can create a new process group and become leader
    • int setpgid(pid_t pid, pid_t pgid);
  • 프로세스 그룹 소멸
    • the last process terminates OR
      • 프로세스 그룹에 속한 프로세스가 다 종료하여 아무것도 없을 때
    • joins another process group
      • 그룹을 떠났을 때
#include <sys/types.h>
#include <unistd.h>
int setpgid(pid_t pid, pid_t pgid);
//프로세스 pid의 프로세스 그룹 ID를 pgid로 설정한다.
//성공하면 0을 실패하면 -1를 반환한다.
  • 새로운 프로세스 그룹을 생성하거나 다른 그룹에 멤버로 참여
    • pid == pgid -> pid가 새로운 그룹을 만듦과 동시에 새로운 그룹 리더가 됨.
    • pid != pgid -> 다른 그룹의 멤버가 됨.
    • pid == 0 -> 호출자의 PID 사용
    • pgid == 0 -> 새로운 그룹 리더가 됨
  • 호출자가 새로운 프로세스 그룹을 생성하고 그룹의 리더가 된다.
    • setpgid(getpid(), getpid());
    • setpgid(0,0);


프로세스 그룹: pgrp2.c

#include <sys/types.h>
#include <unistd.h>
main()
{
    int pid, gid;
    printf("PARENT: PID = %d GID = %d \n", getpid(), getpgrp());
    pid = fork();
    if (pid == 0) {
        setpgid(0, 0);
        printf("CHILD: PID = %d GID = %d \n", getpid(), getpgrp());
    }
}
$ pgrp2
PARENT: PID = 17768 GID = 17768
CHILD: PID = 17769 GID = 17769


프로세스 그룹 사용

  • 프로세스 그룹 내의 모든 프로세스에 시그널을 보낼 때 사용
    • $ kill –9 pid // 9번 시그널 SIGKILL -> pid에게 보냄
    • $ kill –9 0 // 현재 속한 프로세스 그룹 내의 모든 프로세스에게 시그널을 보내 종료 시킴
    • $ kill –9 –pid // –pid 는 프로세스 그룹 pid에 속한 모든 프로세스에게 시그널을 보내 종료 시킴
  • pid_t waitpid(pid_t pid, int *status, int options);
    • pid == -1 : 임의의 자식 프로세스가 종료하기를 기다린다.
    • pid > 0 : 자식 프로세스 pid가 종료하기를 기다린다.
    • pid == 0 : 호출자와 같은 프로세스 그룹 내의 어떤(아무) 자식 프로세스가 종료하기를 기다린다.
    • pid < -1 : pid의 절대값과 같은 프로세스 그룹 내의 어떤 자식 프로세스가 종료하기를 기다린다.

댓글남기기