5.2 fork
fork函数实现进程复制,类似于动物界的单性繁殖,fork函数直接创建一个子进程。这是Linux创建进程最常用的方法。在这一小节中,子进程概念指fork产生的进程,父进程指主动调用fork的进程。
fork后,子进程继承了父进程很多属性,包括:
-
文件描述符:相当与dup,标准输入标准输出标准错误三个文件
-
账户/组ID:
-
进程组ID
-
会话ID
-
控制终端
-
set-user-ID和set-group-ID标记
-
当前工作目录
-
根目录
-
umask
-
信号掩码
-
文件描述符的close-on-exec标记
-
环境变量
-
共享内存
-
内存映射
-
资源限制
但是也有一些不同,包括:
-
fork返回值
-
进程ID
-
父进程
-
进程运行时间记录,在子进程中被清0
-
文件锁没有继承
-
闹钟
-
信号集合
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> int main() { printf("before fork "); // 在父进程中打开的文件描述符 // int fd = open("a.txt", O_RDWR|O_CREAT, 0777); // FILE* fp = fopen("a.txt", "r"); int fd = open("a.txt", O_RDWR); pid_t pid = fork(); // 创建一个新进程 if(pid == 0) { // 子进程可以使用父进程的描述符 // write(fd, "hello", 5); // char ch = fgetc(fp); char ch; read(fd, &ch, 1); printf("ch is %c ", ch); printf("this is in child, ppid=%d ", (int)getppid()); } else if(pid > 0) { // write(fd, "world", 5); char ch; read(fd, &ch, 1); printf("ch is %c ", ch); // 当fork返回值大于0时,说明该进程是父进程 // 此时,返回值就是子进程的pid printf("this is in parent, pid=%d ", (int)getpid()); } else { printf("error fork "); } printf("hello fork "); }
#include <stdio.h> #include <unistd.h> int global_var = 0;//fork()出来的子进程的值改变,不会影响父进程 因为开开辟了新的空间 int main() { int var = 0; int* p = (int*)malloc(sizeof(int)); *p = 0; pid_t pid = fork(); if(pid == 0) { global_var = 100; *p = 100; var = 100; printf("set var "); } else if(pid > 0) { sleep(1); // 确定的结果,就是0 printf("%d ", global_var); printf("var is %d ", var); // 0 printf("*p = %d ", *p); } printf("hello world "); }
#include <stdio.h> #include <unistd.h> void forkn(int n) { int i; for(i=0; i<n; ++i) { pid_t pid = fork(); if(pid == 0) break; } } int main() { forkn(10); printf("hello world "); }
5.3 进程终止
进程有许多终止方法:
方法 | |
---|---|
main函数return | 正常退出 |
调用exit或者_Exit或者_exit | 正常退出 |
在多线程程序中,最后一个线程例程结束 | 正常退出 |
在多线程程序中,最后一个线程调用pthread_exit | 正常退出 |
调用abort | 异常退出 |
收到信号退出 | 异常退出 |
多线程程序中,最后一个线程响应pthread_cancel | 异常退出 |
当进程退出时,内核会为进程清除它申请的内存,这里的内存是指物理内存,比如栈空间、堆、代码段、数据段等,并且关闭所有文件描述符。
一般来说,进程退出时,需要告诉父亲进程退出的结果,如果是正常退出,那么这个结果保存在内核的PCB中。如果是异常退出,那么PCB中保存退出结果的字段,是一个不确定的值。因此程序员应该避免程序的异常退出。
进程退出时,除了它的PCB所占内存,其他资源都会清除。
5.4 wait和waitpid
一个进程终止后,其实这个进程的痕迹还没有完全被清除,因为还有一个PCB在内核中,如果不回收,那么会导致内存泄漏。父进程可以调用wait函数来回收子进程PCB,并得到子进程的结果。
wait
是一个阻塞调用,它的条件是一个子进程退出或者一个子进程有状态变化。wait
得到的status,包含了子进程的状态变化原因和退出码信息等等。
#include <stdio.h> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t pid = fork(); if(pid == 0) { sleep(1); printf("child process "); return 18; } else if(pid > 0) { printf("parent process "); // 等待子进程结束,并且回收子进程的PCB int status; wait(&status); // 如何得到子进程的返回值 if(WIFEXITED(status)) { printf("normal child process exit "); // 正常退出 int code =WEXITSTATUS(status); printf("code is %d ", code); } else if(WIFSIGNALED(status)) { printf("signal "); } else if(WIFSTOPPED(status)) { printf("child stopped "); } else if(WIFCONTINUED(status)) { printf("child continue... "); } printf("after wait "); } return 0; }
wait和waitpid可能会阻塞父进程,所以一般使用SIGCHLD信号来监控子进程
5.5 僵尸进程和孤儿进程
5.5.1 僵尸进程
是指已经退出的进程,但是父进程没有调用wait回收的子进程。僵尸进程没有任何作用,唯一的副作用就是内存泄漏。如果父进程退出,那么它的所有僵尸儿子会得到清理,因此僵尸进程一般指那些用不停歇的后台服务进程的僵尸儿子。
程序员应该避免僵尸进程的产生。
#include <stdio.h> #include <unistd.h> int main() { pid_t pid = fork(); if(pid == 0) { // 子进程什么事儿都不干,退出了,此时子进程是僵尸进程 } else if(pid > 0) { getchar(); // 父进程不退出 } return 0; }
5.5.2 孤儿进程
父进程退出了,而子进程没有退出,那么子进程就成了没有父亲的孤儿进程。孤儿进程不会在系统中出现很长时间,因为系统一旦发现孤儿进程,就会将其父进程设置为init进程。那么将来该进程的回收,由init来负责。
5.6 exec
exec函数执行一个进程,当一个进程调用exec后,调用该函数的进程的虚拟地址空间的代码段、数据段、堆、栈被释放,替换成新进程的代码段、数据段、堆、栈,而PCB依旧使用之前进程的PCB。这个函数用中文来说就是鸠占鹊巢。
exec后使用的是同一个PCB,所以exec之后和之前,由很多进程属性是相同的,包括:
-
进程ID和父进程ID
-
账户相关
-
进程组相关
-
定时器
-
当前目录和根目录
-
umask
-
文件锁
-
信号mask
-
未决的信号
-
资源限制
-
进程优先级
-
进程时间
-
没有close-on-exec属性的文件描述符
使用fork和exec来执行一个新程序
#include <unistd.h> #include <stdio.h> // execle, e表示环境变量environ // int main(int argc, char* argv[]) { char* args[] = { "/bin/ls", "-a", "-l", NULL }; execv("/bin/ls", args); } int main2(int argc, char* argv[]) { // p表示在PATH的环境变量中寻找这个程序 execlp("ls", "ls", NULL); } int main1(int argc, char* argv[]) { // 执行一个程序 execl("/bin/ls", "/bin/ls", "-a", "-l", NULL); // 该函数不会被执行 printf("hello world "); }
#include <stdio.h> #include <fcntl.h> #include <sys/types.h> #include <unistd.h> int main() { // fd is 3 int fd = open("exec.txt", O_RDWR|O_CREAT|O_CLOEXEC, 0777); execl("./exec_test", "./exec_test", NULL); }
函数后缀 | 解析 |
---|---|
l | list 用不定参数列表来表示命令参数,如果用不定参数列表,那么用NULL表示结束 |
v | vector 用数组来传递命令行参数 |
p | path 表示程序使用程序名即可,在$PATH中搜索该程序,不带p的需要提供全路径 |
e | environ 表示环境变量 |
补充:不定参数
不定参数函数定义:
int main() { int a = add(3, 12, 13, 14); int b = add(2, 12, 13); int c = add(4, 12, 13, 14, 15); printf("%d, %d, %d ", a, b, c); char* p = concat("abc", "bcd", NULL); printf("p is %s ", p); // 最后的NULL,被称之为哨兵 p = concat("aaaa", "bbbb", "cccc", NULL); printf("p is %s ", p); }
#include <stdio.h> #include <fcntl.h> #include <sys/types.h> // 如果没有__VA_ARGS__不带##,表示__VA_ARGS__至少要表示一个参数 // #define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, __VA_ARGS__) // __VA_ARGS__如果有##,表示可以没有参数 #define mylog(fmt, ...) printf("[%s:%d] "fmt, __FILE__, __LINE__, ##__VA_ARGS__) int main() { int fd = open("a.txt", O_RDWR); if(fd < 0) { mylog("error open file "); } }
#include <stdio.h> // 转字符串 abc "abc" #define STR(a) #a // 拼接标识符 #define CC(a, b) a##b int main() { int abcxyz = 100; printf("%d ", CC(abc, xyz)); }
5.8 账户和组控制
5.9 进程间关系
在Linux系统中,进程间除了有父子关系,还有组关系、Session关系、进程和终端进程关系。设计这些关系是为了更好的管理进程。
5.9.1 Session
一次登陆算一个session,exit命令可以退出session,session包括多个进程组,一旦session领导退出,那么一个session内所有进程退出(它的所有进程收到一个信号)。
#include <unistd.h> int main() { pid_t pid = fork(); if(pid == 0) { // 独立一个session setsid(); } while(1) { sleep(1); } }
5.9.2 进程组
在终端执行进程,就会生成一个进程组。执行的进程fork之后,子进程和父进程在一个组中。
进程组长退出后,进程组的其他进程的组号依旧没有变化。
5.10 练习
5.10.1 fork任意个子进程。
5.10.2 使用多进程加速文件拷贝
使用-job
定义进程数量,加速文件拷贝。
5.10.3 实现自定义终端
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> // ls // mkdir aaa // cp ../aa bb // cd void handle_cmd(char* cmd) { char* args[1024]; char* p = strtok(cmd, " "); int i = 0; while(p) { args[i++] = p; p = strtok(NULL, " "); } args[i] = NULL; // 表示参数结束位置 if(strcmp(args[0], "cd") == 0) { // 切换当前目录 chdir(args[1]); return; } pid_t pid = fork(); if(pid == 0) { execvp(args[0], args); // 如果命令执行失败,应该让子进程退出 printf("invalid command "); exit(0); } else { wait(NULL); } } int main() { while(1) { printf("myshell> "); // 等待用户输入 char buf[4096]; fgets(buf, sizeof(buf), stdin); buf[strlen(buf)-1] = 0; // remove if(strlen(buf) == 0) { continue; } handle_cmd(buf); } }
5.11 函数和命令
5.11.1 函数
fork:创建子进程
exec:执行新的程序
wait/waitpid:等待子进程结束,回收子进程PCB内存。
va_list:
va_start:定义指向不定参数的第一个参数的地址
va_arg:从参数列表中获取一个参数,并且让指针指向下一个参数
va_end:清除ap