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