一、进程的产生(fork)
fork(2) 系统调用会复制调用进程来创建一个子进程,在父进程中 fork 返回子进程的 pid,在子进程中返回 0。
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
fork 后子进程不继承未决信号和文件锁,资源利用量清 0。 由于进程文件描述符表也继承下来的,所以可以看到父子进程的输入输出指向都是一样的,这个特性可以用于实现基本的父子进程通信。
init() 是所有进程的祖先进程,pid = 1。
例子(fork_test.c) :
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
pid_t pid;
printf("Begin\n");
//fflush(); //!!!重要
if ((pid = fork()) == 0) {
// child
printf("child process executed\n");
exit(0);
} else if (pid < 0) {
perror("fork");
exit(1);
}
// father
//sleep(1);
printf("parent process executed\n");
exit(0);
}
运行结果:
注意:父子进程的运行顺序不能确定,由调度器的调度策略决定。
面试题:当将输出重定向到文件里面时,Begin 为什么打印了两次?如下图:
答案:输出到终端默认是行缓冲模式,加 “\n” 即可刷新缓冲区,但由于重定向到文件是写文件,而写文件是全缓冲,所以 “\n” 无法刷新缓冲区,所以需要在 Begin 后加上 fflush() 来强制刷新缓冲区。
例子(primes_fork.c,通过子进程来计算质数):
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
int main()
{
int max = 100;
pid_t pid;
for (int i = 2; i <= max; i++) {
if ((pid = fork()) == 0) {
// child
int flag = 1;
for (int j = 2; j <= i / 2; j++) {
if (i % j == 0) {
flag = 0;
break;
}
}
if (flag) {
printf("%d\n", i);
}
exit(0);
} else if (pid < 0) {
perror("fork");
exit(1);
}
}
exit(0);
}
通过 man ps 可以找到进程的所有状态信息:
- D:不可中断的睡眠态(通常是 IO);
- I:空闲的内核线程;
- R:运行态或可运行态;
- S:可中断的睡眠态(等待事件的完成);
- T:被控制信号停止;
- X:死亡态;
- Z:僵尸(zombie)进程,已终止但未被其父亲接收;
其中父进程如果不使用 waitpid 接收子进程状态,会导致子进程终止后变成僵尸态,会占用 pid 号,父进程终止后内核会自动将子进程交付给 init 进程,等待子进程终止后为其 “收尸”。
二、进程的消亡及释放资源(wait、waitpid)
wait(2) 和 waitpid(2) 可以等待进程状态发生变化。
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
wait(2) 成功时返回终止的子进程的 pid,不需要指定特定的子进程 pid,并且需要死等(阻塞)。 若 wstatus 非空,则其可以一些宏函数指示进程的状态:
- WIFEXITED(wstatus):若子进程正常终止则返回真(exit(3)、_exit(2) 或从 main 函数返回);
- WEXITSTATUS(wstatus):返回子进程的退出状态码,前置条件是 WIFEXITED(wstatus) 必须首先为真;
- WIFSIGNALED(wstatus):若子进程被信号终止了则返回真;
- WTERMSIG(wstatus):检测终止子进程的信号值,前置条件是 WIFSIGNALED(wstatus) 为真;
waitpid(2) 相比于 wait(2) 可以指定等待的子进程(pid),并且可以指定一些选项(options):
- WNOHANG:如果没有子进程退出则立即返回(非阻塞);
进程分配任务的方法:
- 分块(每个线程一部分任务);
- 交叉分配(依次给每个线程分配任务);
- 池(往任务池里面扔任务,线程从池中抢任务);
三、exec 函数族
exec 函数族可以用来执行一个二进制可执行文件。
#include <unistd.h>
extern char **environ;
/* 需要给出文件路径 */
int execl(const char *pathname, const char *arg, ...
/* (char *) NULL */);
/* 只需要文件名称,然后去环境变量environ中寻找 */
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *pathname, const char *arg, ...
/*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
exec 函数族会将当前进程映像替换为新的进程映像。所以在 exec 后的代码不会执行。
在 exec 之前需要 fflush(),和前面 1.1 的例子一样,写文件是全缓冲,会导致打印的内容还没写入到文件就被 exec 替换掉了进程映像。
例子,使用 fork + exec 来实现一个简单的 shell(myshell.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <glob.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#define DELIMS " \t\n"
struct cmd_st
{
glob_t globres;
};
static void prompt(void)
{
printf("mysh$ ");
}
static void parse(char *line, struct cmd_st *cmd)
{
char *tok;
int i = 0;
while (1) {
tok = strsep(&line, DELIMS);
if (tok == NULL)
break;
if (tok[0] == '\0') // empty str
continue;
glob(tok, GLOB_NOCHECK | GLOB_APPEND * i, NULL, &cmd->globres);
i = 1;
}
}
int main()
{
char *linebuf = NULL;
size_t linebuf_size = 0;
struct cmd_st cmd;
pid_t pid;
while (1) {
prompt(); // 打印提示符
if (getline(&linebuf, &linebuf_size, stdin) < 0) {
break;
}
parse(linebuf, &cmd); // 解析命令
/* extern cmd */
{
pid = fork();
if (pid < 0) {
perror("fork");
exit(1);
}
/* child process */
if (pid == 0) {
execvp(cmd.globres.gl_pathv[0], cmd.globres.gl_pathv);
perror("exec");
exit(1);
}
wait(NULL);
}
}
exit(0);
}
可以在 /etc/passwd 文件里修改用户的登录 shell,十分有趣:
四、守护进程
持续运行在后台,等待处理请求的进程。一次成功的登录会产生一个会话(session)。
管道符:把第一个命令的标准输出作为第二个命令的标准输入(ls | more)。
Linux--setsid() 与进程组、会话、守护进程
例子(mydaemon.c):
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#define FILENAME "/tmp/out"
static void daemonize(void)
{
pid_t pid;
int fd;;
if ((pid = fork()) < 0) {
perror("fork");
exit(1);
}
if (pid == 0) { // child process
if ((fd = open("/dev/null", O_RDWR)) < 0) {
perror("open");
exit(1);
}
dup2(fd, 0);
dup2(fd, 1);
dup2(fd, 2);
if (fd > 2)
close(fd);
setsid();
// change working directory
chdir("/"); // preventing "device is busy"
// umask(0);
return;
} else {
exit(0);
// the daemon process's parent will be the init process
}
}
int main()
{
FILE* fp = NULL;
// init daemon process
daemonize();
// the task of daemon process
if ((fp = fopen(FILENAME, "w")) == NULL) {
perror("fopen");
exit(1);
}
for (int i = 0; ; i++) {
fprintf(fp, "%d\n", i);
fflush(fp); // writting file is full buffer, so we should flush the buffer after printf()
sleep(1);
}
exit(0);
}
编译运行程序后使用 ps -axj 可以看到 daemon 进程在后台运行,但是发现其 PPID(父进程 pid)不是 init 进程的 pid 1,查了一下发现是在 Ubuntu18.04 系统中,孤儿进程会被 “/lib/systemd/systemd --user” 进程领养。
pid 为 1097 对应的进程为 /lib/systemd/systemd --user:
syslogd 服务:
- openlog() 打开系统日志的连接;
- syslog() 提交日志;
- closelog() 关闭系统日志的连接;