Process and Thread

进程的创建

fork() 是类 Unix 系统中创建进程的基本操作,其具有 “调用一次,返回两次”、“复制但分离的地址空间”、“文件共享”、以及 “并发执行” 这些特点。运行并理解如下程序的输出,同时可使用 strace -f 来追踪程序执行过程中(包括子进程)产生的系统调用。

#include <stdio.h>
#include <unistd.h>

int count = 0;

int main(void) {
  fork();
  fork();

  for (int i = 0 ; i < 1000 ; i++)
    count++;
  
  printf("process (%d): %d\n", getpid(), count);
  return 0;
}
#include <stdio.h>
#include <unistd.h>

int main(void) {
  for (int i = 0 ; i < 2 ; i++) {
    fork();
    printf("hello\n");
  }
  return 0;
}

进程的改变

很多时候父子进程并不是同样的执行逻辑,例如 Shell 创建进程并不是为了创建一个 Shell 子进程,而是 “加载” 某个可执行文件来运行,execve() 就是这样的系统调用。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(int argc, char *argv[]) {
  int rc = fork();
  if (rc < 0) {
    perror("fork failed\n");
    exit(1);
  } else if (rc == 0) {
    printf("[%d] child process\n", (int) getpid());
    // run word count program /bin/wc with arguments and environment variables
    char *args[] = {"/bin/wc", "fork.c", NULL}; 
    char *envp[] = {"KEY=Value", NULL};
    execve(args[0], args, envp);
    // the followings should not be executed
    printf("Hello?\n");
  } else {
    wait(NULL);
    printf("[%d] parent process\n", (int) getpid());
  }
  return 0;
}

两个系统调用

类 Unix 系统使用 fork() + execve() 的方式来运行一个新的应用程序,这种方式可以在执行 fork() 之后、但在执行 execve() 之前对子进程的执行环境进行修改,从而实现一些特定的能力。

重定向

例如,可以在 execve() 之前通过精心操作文件描述符 (file descriptor) 来实现标准输出 stdout 的重定向 (例如实现 Shell 中的 wc fork.c > output.txt)。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/wait.h>
#include <sys/stat.h>

int main(int argc, char *argv[]) {
  int rc = fork();
  if (rc < 0) {
    perror("fork failed\n");
    exit(1);
  } else if (rc == 0) {
    // redirect standard output to a file
    close(STDOUT_FILENO); 
    open("./output.txt", O_CREAT|O_WRONLY|O_TRUNC, S_IRWXU);
    // now run "wc"
    char *args[] = {"wc", "fork.c", NULL}; 
    execvp(args[0], args);    
  } else {
    wait(NULL);
  }
  return 0;
}

管道

通过 pipe() 系统调用,我们可以创建一个匿名管道(pipe)对象来将父子进程的输出和输入连接起来,通过父进程向管道 write()、子进程从管道 read() 的方式实现进程间的通信 (例如实现 Shell 中的 cat a.txt b.txt | sort)。下面代码给出了创建管道的一个简单示例:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

#define MSGSIZE 10
char *msg[] = {"Message #1", "Message #2", "Message #3"};

int main(void) {
  char inbuf[MSGSIZE];
  int fd[2];

  if (pipe(&fd[0]) < 0) {
    perror("pipe failed!\n");
    exit(1);
  } else { 
    printf("pipe file descriptor: fd[0] = %d, fd[1] = %d\n", fd[0], fd[1]);
    
    int pid = fork();
    if (pid < 0) {
      perror("fork failed!\n");
      exit(1);
    } else if (pid > 0) {
      // parent process (writer)
      close(fd[0]);
      for (int i = 0; i < 2; i++) {
        write(fd[1], msg[i], MSGSIZE);
        printf("Send: %s\n", msg[i]);
        sleep(2);
      }
      wait(NULL);
    } else {
      // child process (reader)
      close(fd[1]);
      for (int j = 0 ; j < 3 ; j++) {
        int ret = read(fd[0], inbuf, MSGSIZE);
        printf("Receive: %s (ret = %d)\n", inbuf, ret);
      }
    }
  }
  return 0;
}

进程的终止

从 main 函数中返回是进程最常见的终止方式,除此之外还可以通过 exit(0)_exit(0)syscall(SYS_exit, 0) 等方式、以及异常终止。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/syscall.h>
#include <assert.h>

void func() {
  printf("Goodbye\n");
}

int main(int argc, char *argv[]) {
  atexit(func);
  // we can use $? to reference the return code in bash
  if (argc < 2) {
    return 1;
  }
  // libc exit
  if (strcmp(argv[1], "exit") == 0) {
    exit(0);
  }
  // immediate operating system exit, provided by libc
  if (strcmp(argv[1], "_exit") == 0) {  
    _exit(0);
  }
  // even more operating system exit
  if (strcmp(argv[1], "syscall") == 0) {
    syscall(SYS_exit, 0);
  }
  // abort(): SIGABRT
  if (strcmp(argv[1], "abort") == 0) {  
    assert(1 == 0);
  }
}

父进程等待子进程

父进程可以通过 wait() 来等待子进程执行结束后再继续运行。如果父进程没有 wait() 子进程,则子进程执行结束后会进入僵尸状态 (Zombie Process);如果父进程先于子进程结束,则子进程会变为孤儿进程 (Orphan Process)。尝试修改下列代码来分别制造僵尸进程和孤儿进程。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>

int main(void) {
  int rc = fork();
  if (rc < 0) {
    perror("fork failed\n");
    exit(1);
  } else if (rc == 0) {
    printf("[%d] child process\n", (int) getpid());
    exit(0);
  } else {
    int status;
    wait(&status);
    printf("[%d] parent process, status = %d\n", (int) getpid(), WEXITSTATUS(status));
  }
  return 0;
}

信号 (Signal)

信号也是进程间通信的一种机制,用于通知进程发生了某个事件,并以进程定义的方式处理。 Linux Shell 中可以使用 kill 来向进程发送各种 signal(kill -l 展示 signal 名称和编号),而应用程序可以实现特定的 signal handler 来进行相应处理(例如,忽视 Ctrl+C 产生的 SIGINT 信号)。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <signal.h>

static void sigalrm_handler(int);
static void sigint_handler(int);

int main(void) {
  signal(SIGHUP, SIG_IGN);          // ignore a signal
  signal(SIGALRM, sigalrm_handler); // register a signal handler
  signal(SIGINT, sigint_handler);   
  
  printf("Process ID: %d\n", getpid());

  while(1) {
    alarm(3);   // send a SIGALRM signal after 3 seconds
    pause();    // suspend until it receives a signal
  }
  return 0;
}

static void sigalrm_handler(int sig_no) {
  printf("Receive an alarm!\n");
}

static void sigint_handler(int sig_no) {
  printf("Haha!\n");
}

用户空间的线程切换

下述代码给出了在用户空间实现线程切换的简单示例,通过合适的封装就可以帮助我们在用户空间实现线程模型。

// thread.S
.section .data
msg1:   .asciz "this is thread 1\n"
len1 = . - msg1
msg2:   .asciz "this is thread 2\n"
len2 = . - msg2

.section .bss
        .space 100     # stack for thread 1
stack1: .space 100     # stack for thread 2
stack2:

sp1:    .space 8       # stack pointer for thread 1
sp2:    .space 8       # stack pointer for thread 2

.section .text
.global _start
_start:
    lea stack1, %rsp
    pushq %rax
    pushq %rdi
    pushq %rsi
    pushq %rdx
    movq %rsp, (sp1)

    lea stack2, %rsp
    pushq $thread2
    pushq %rax
    pushq %rdi
    pushq %rsi
    pushq %rdx
    movq %rsp, (sp2)

thread1:
    movq (sp1), %rsp
    movq $1, %rax       # write syscall
    movq $1, %rdi       # file descriptor
    movq $msg1, %rsi    # message to print

    call yield

    movq $len1, %rdx    # length of message
    syscall

    movq $60, %rax      # exit syscall
    movq $0, %rdi       # exit status
    syscall             

thread2:
    movq $1, %rax       # write syscall
    movq $1, %rdi       # file descriptor
    movq $msg2, %rsi    # message to print
    movq $len2, %rdx    # length of message
    syscall

    call return_thread1      

yield:
    pushq %rax            # save thread1 registers
    pushq %rdi
    pushq %rsi
    pushq %rdx
    movq %rsp, (sp1)      # thread1 stack -> thread2 stack
    movq (sp2), %rsp      
    popq %rdx             # restore thread2 registers
    popq %rsi
    popq %rdi
    popq %rax
    ret

return_thread1:
    movq (sp1), %rsp      # set thread1 stack
    popq %rdx             # restore thread1 registers
    popq %rsi
    popq %rdi
    popq %rax
    ret
cpp thread.S > thread.i
as thread.i -o thread.o
ld thread.o