kill() System Call
Sends a signal "sig" to a process "pid"
pid에 해당하는 프로세스에 sig에 해당하는 signal을 보내는 시스템 콜 메소드입니다.
- int kill(pid_t pid, int sig)
만약 성공했다면 0을, 실패하거나 오류가 발생했다면 -1을 return합니다.
+ 이때, sig값에는 signal ID를 넣어주어도 되지만, kill()을 담고있는 header file에 #define을 통해 signal NAME마다 signal ID가 맵핑되어있기 때문에, signal NAME을 넣어주어도 됩니다.
아래는 kill()의 예시입니다:
void fork12()
{
pid_t pid[N];
int i, child_status;
for (i=0; i<N; i++)
if ((pid[i] = fork()) == 0)
while(1); /* Child infinite loop */
/* Parent terminates the child processes */
for (i=0; i<N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
/* Parent reaps terminated children */
for (i=0; i<N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abmormally\n", wpid);
}
}
signal() System Call
Modifies the default action associated with the receipt of signal "signum"
"signum"에 해당하는 signal을 받았을 때 수행하는 기본 행동을 수정하는 메소드입니다.
- sighandler_t signal(int signum, sighandler_t handler)
이때 sighandler_t는 functional pointer로 함수의 포인터값이 들어갑니다. 이때 sighandler_t의 형식은 반드시 return이 void이고, int타입 파라미터를 하나만 받는 함수 포인터이어야 합니다.
만약 변경에 성공한다면 기존에 있었던 signal handler의 함수형 포인터값을 반환합니다.
주의할 점은 SIGKILL이나 SIGSTOP의 기본 handler는 수정할 수 없으며, 수정하려 하면 SIG_ERR를 반환합니다. 따라서 조건문으로 올바르게 변경했는지 확인해야합니다.
values for handler
handler의 값으로는 다음에 해당하는 값들이 있습니다. 이 값을 통해 signal handler를 수정할 수 있습니다:
- SIG_IGN: ignore signals of type signum
- SIG_DFL: revert to the default action on receipt of signals of type signum(* 이는 handler를 원상 복구시킬 때 사용합니다.)
아래는 signal()의 예시입니다.
void int_handler(int sig)
{
printf("Process %d received signal %d\n", getpid(), sig);
_exit(0);
}
void fork13()
{
pid_t pid[N];
int i, child_status;
// 추가
signal(SIGINT, int_handler);
for (i=0; i<N; i++)
if ((pid[i] = fork()) == 0)
while(1); /* Child infinite loop */
/* Parent terminates the child processes */
for (i=0; i<N; i++) {
printf("Killing process %d\n", pid[i]);
kill(pid[i], SIGINT);
}
/* Parent reaps terminated children */
for (i=0; i<N; i++) {
pid_t wpid = wait(&child_status);
if (WIFEXITED(child_status))
printf("Child %d terminated with exit status %d\n", wpid, WEXITSTATUS(child_status));
else
printf("Child %d terminated abmormally\n", wpid);
}
}
int_handler의 파라미터인 int sig는 어떤 signal때문에 이 handler가 호출되었는지를 알려줍니다.
또한 fork()를 하기전에 부모 프로세스에서 handler를 수정했는데, 이를 통해 fork()를 하면 부모의 handler까지 모두 복사해서 생성됨을 알 수 있습니다.
결과는 아래와 같습니다.
pause() System Call
Causes the calling process (or thread) to sleep until a signal is delivered
다른 signal을 받을 때까지 프로세스나 쓰레드를 sleep합니다.
아래는 pause()의 예시입니다.
int main(void)
{
....
printf("pid: %d\n", getpid());
....
pause();
printf("pause() returned\n");
....
return 0;
}
아래는 결과입니다.
SIGKILL을 받으면 default action에 의해 바로 종료되므로, pause() 아래의 코드들이 실행되지 않습니다.
Blocking in Signal Handler
다음의 예시를 통해 handler가 호출되어 실행되는 동안 모든 프로세스가 멈추는지를 알 수 있습니다.
void sig_handler(int signum)
{
printf("signal handler started\n");
sleep(100); // 100초 동안 sleep
}
int main(void)
{
....
signal(SIGALRM, sig_handler);
printf("signal handler for SIGALRM registered ... \n");
/* Not guaranteed */
kill(getpid(), SIGALRM);
/* Do something */
return 0;
}
결과는 멈춥니다.
handler가 동작하는 동안 main process의 logical flow는 멈추기 때문에, 최대한 간단하게 handler를 만들어야합니다.
다음의 예시를 통해 SIGKILL과 SIGSTOP의 handler를 수정할 수 있는지를 알 수 있습니다.
void sig_handler(int signum)
{
printf("signal handler started\n");
sleep(100);
}
int main(void)
{
....
signal(SIGKILL, sig_handler);
kill(getpid(), SIGKILL);
/* Do something */
return 0;
}
결과는 종료됩니다.
앞서 말했듯, SIGKILL이나 SIGSTOP의 handler를 수정하려하면 return 값이 SIG_ERR로 return되며 handler는 수정되지 않습니다. 따라서 return value를 확인해야합니다.
Shell Programs
A shell is an application program that runs programs on behalf of the user
- sh: Original Unix shell
- csh/tcsh: BSD Unix shell
- bash: "Bourne-Again" Shell (default Linux shell)
다음의 예시를 통해 shell을 프로그래밍한 모습을 볼 수 있습니다.
void eval(char *cmdline)
{
char *argv[MAXARGS]; /* Argument list execve() */
char buf[MAXLINE]; /* Holds modified command line */
int bg; /* should the job run in bg or fg? */
pid_t pid;
strcpy(buf, cmdline);
bg = parseline(buf, argv);
if(argv[0] == NULL)
return; /* Ignore empty lines */
if(!builtin_command(argv)){
if((pid = fork()) == 0) {
if(execv(argv[0], argv) < 0) {
printf("%s, Command not found.\n", argv[0]);
exit(0);
}
}
/* Parent waits for foreground job to terminate */
if(!bg){
int status;
if (waitpid(pid, &status, 0) < 0)
unix_error("waitfg: waitpid error");
}
else
printf("%d %s", pid, cmdline);
}
return;
}
int main()
{
char cmdline[MAXLINE]; /* command line */
while(1){
/* read */
printf("> ");
fgets(cmdline, MAXLINE, stdin);
if (feof(stdin))
exit(0); /* Ctrl+D or Ctrl+Z */
/* evaluate */
eval(cmdline);
}
}
char *argv[MAXARGS]는 char*의 배열이므로, string의 배열임을 알 수 있습니다.
buf는 cmdline을 받을 버퍼입니다.
bg는 bg실행인지 fg실행인지 알려줄 int타입 value입니다.
strcpy()를 통해 cmdline을 buf에 넣어주고,
parseline()을 통해 buf에 끝에 '&'가 있는지 확인, 있으면 bg=1, 없으면 bg=0을 return, argv에 buf의 값들을 끊어서 넣어줍니다.
만약 다음과 같은 cmdline이 입력으로 들어오면 argv의 값은 다음과 같습니다:
$ my_pgm 10 5 a
argv[0] = "my_pgm"
argv[1] = 10
argv[2] = 5
argv[3] = 'a'
만약 argv[0]값, 즉 명령어 부분이 NULL이라면, 그냥 eval메소드를 종료합니다.
그리고 만약 argv가 built-in 명령어라면 shell에 미리 정해져있는 명령어를 수행하면 됩니다. 하지만 우리 예시에서는 그런 경우는 살펴보지 않을것이기 때문에 builtin_command(argv)는 참입니다.
fork()를 통해 자식 프로세스를 만들고 execv()를 통해 프로세스를 실행해줍니다. 만약, 정상적으로 실행되지 않아 음수값이 return되었다면, error메시지를 출력하고 프로세스를 종료합니다.
만약 fg에서 실행중이라면 생성한 자식 프로세스가 종료될 때까지 wait하고, reap해줍니다. 만약 waitpid()가 정상적으로 실행되지 않아 음수값이 return되었다면, error메시지를 출력합니다.
만약 bg에서 실행중이라면 자식 프로세스의 pid와 cmdline을 출력해주고 eval함수를 종료합니다.
이때 문제가 있습니다.
fg에서 실행했던 프로세스는 waitpid()를 통해 reaping해주지만,
bg에서 실행했던 프로세스는 wait()이 없어 zombie가 되버리는 문제가 있습니다.
이렇게 되면 메모리의 누수가 지속적으로 일어나게 됩니다.
따라서 이를 해결하기 위해 SIGCHLD에 대한 handler를 수정합니다.
void handler(int sig)
{
int olderrno = errno;
pid_t pid;
while((pid = waitpid(-1, NULL, 0)) > 0){ // waitpid(-1, NULL, 0)은 wait(NULL, 0)과 같다.
/* Reap child */
deletejob(pid); /* Delete the child from the job list */
if(errno != ECHILD)
sio_error("waitpid error");
errno = olderrno;
}
int main(int argc, char **argv)
{
int pid;
signal(SIGCHLD, handler);
initjobs(); /* Initialize the job list */
while(1){
if((pid = fork()) == 0){
/* Child */
execv("/bin/data", argv);
}
addjob(pid); /* Add the child to the job list */
}
}
이때 initjobs(), addjob(), deletejob()은 job list를 관리하는 메소드입니다.
job list는 현재 실행중인 job들의 pid를 저장해놓은 linked list입니다.
위 예시의 문제점은 크게 두가지 입니다:
- Race condition
- 프로세스간의 속도의 차이로 인해 addjob()이 불리기 전에, deletejob()이 불릴 수 있습니다.
- job list에 접근하는 포인터가 겹칠 수 있습니다. (* addjob()과 deletejob()이 겹치면 데이터의 일관성이 유지될 수 없습니다.)
- Blocking in waitpid
- while((pid = waitpid(-1, NULL, 0)) > 0)에서 자식 프로세스가 종료될 때까지 기다릴 수 있다. 이 때문에 main process의 logical flow가 멈출 수 있다.
'[학교 수업] > [학교 수업] 시스템 프로그래밍' 카테고리의 다른 글
[시스템 프로그래밍] Signal Examples (1) | 2024.10.06 |
---|---|
[시스템 프로그래밍] Race Condition (0) | 2024.09.28 |
[시스템 프로그래밍] Signals (0) | 2024.09.20 |
[시스템 프로그래밍] Linux Commands (0) | 2024.09.20 |
[시스템 프로그래밍] Linux Installation (0) | 2024.09.13 |