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 if execve is succesfully 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);
}
} 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;
}
POSIX API
posix_spwan()
是基于 POSIX 标准的创建进程并执行某个特定文件的方法,其提供一个统一的接口来实现 fork()
+ execve()
的能力。
#include <stdio.h>
#include <stdlib.h>
#include <spawn.h>
#include <sys/wait.h>
#include <fcntl.h>
#include <unistd.h>
#include <errno.h>
extern char **environ; // environment variables
int main(void) {
pid_t pid;
char *argv[] = {"wc", "fork.c", NULL};
// create a posix_spawn_file_actions_t object
posix_spawn_file_actions_t file_actions;
if (posix_spawn_file_actions_init(&file_actions) != 0) {
perror("posix_spawn_file_actions_init");
return 1;
}
// open a file
int fd = open("output.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
if (fd < 0) {
perror("open");
return 1;
}
// redirect stdout to the above file
if (posix_spawn_file_actions_adddup2(&file_actions, fd, STDOUT_FILENO) != 0) {
perror("posix_spawn_file_actions_adddup2");
return 1;
}
// create a child process and execute it
int ret = posix_spawnp(&pid, argv[0], &file_actions, NULL, argv, environ);
if (ret != 0) {
errno = ret;
perror("posix_spawnp");
return 1;
}
close(fd);
posix_spawn_file_actions_destroy(&file_actions);
// wait for child
wait(NULL);
return 0;
}