一、进程的产生(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) {// childprintf("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) {// childint 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 strcontinue;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 processif ((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 directorychdir("/"); // 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 processdaemonize();// the task of daemon processif ((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() 关闭系统日志的连接;