8.3函数fork每一个进程都有一个非负的唯一进程ID,且这个ID是可以复用的。
系统中有专有的进程:
ID为0:调度进程,也称交换进程,也叫系统进程ID为1:init进程,通常位于/sbin目录下. init通常读取与系统的初始化文件(/etc/rc*或etc/inittab文件,及在/etc/init.d中的文件),并引导系统到一个状态. init进程绝不会终止,它是一个普通用户进程,但以超级用户特权运行..
各种ID可通过下面的接口读取
8.4 程序vforkpid_t ret = fork(void);1. 调用会创建一个一样的子进程
2.失败 ret < 0 ; 父进程 ret > 0; 子进程 ret = 0;
使用Demo
#include#include #include #include int main() { pid_t pid; printf("Begin %dn", getpid()); pid_t tmp = fork(); if(tmp < 0) { // fail perror("fail"); exit(1); } if (tmp == 0) { //child printf("child is working %dn", getpid()); } else { //parent // sleep(2); printf("parent is working %dn", getpid()); } printf("End %dn", getpid()); exit(0); } 结果:
zion6135@zion6135-VirtualBox:~/Desktop$ ./a.out Begin 31828 parent is working 31828 End 31828 zion6135@zion6135-VirtualBox:~/Desktop$ child is working 31829 End 31829 ls a.out test.cpp zion6135@zion6135-VirtualBox:ps axf 31874 pts/1 S+ 0:00 | | _ ./a.out 31875 pts/1 S+ 0:00 | | _ ./a.out注:子进程,父进程的运行先后不是绝对的, 是由调度策略来决定谁先被调用, 如果想让child先运行可以让parent先sleep(2);来让子进程先运行.(但sleep这种方法是有问题的,不能这么用. )
重映射打印两次Begin
zion6135@zion6135-VirtualBox:~/Desktop$ ./a.out > ~/Desktop/tmp #打印两次Begin zion6135@zion6135-VirtualBox:~/Desktop$ cat tmp Begin 31943 parent is working 31943 End 31943 Begin 31943 child is working 31944 End 31944 zion6135@zion6135-VirtualBox:为什么会终端和重映射打印结果不一样呢?
因为终端是标准输出设备, 默认是行缓冲模式,所以终端中的打印语句有'n',会刷新缓存.
而重映射到文件,默认是全缓冲模式,所以不管有没有'n'都不会刷新缓存,
当调用fork()的时候, 父子进程都有一句缓冲的 printf("Begin %dn");的语句, 所以子进程父进程的结果都是Begin 31943所以需要在fork()之前加上fflush(NULL)来刷新缓冲区.来解决这个问题.
[注] 说明fork()之后,父子进程的缓冲也被复制了.
fork实例2:质数计算求取,通过fork来计算一个数:说明fork有时候并不能提高程序速度
// 质数计算求取,通过fork来计算一次 #if 1 #include#include #include #include #define LEFT 30000000 #define RIGHT 30000200 int main() { int mark, j; for (int i = LEFT; i <= RIGHT; i++) { pid_t pid = fork(); // 父-->子-->孙-->。。。共200次 if (pid < 0) { perror("error"); exit(1); } if (pid == 0) { mark = 1; for (j = 2; j < i / 2; j++) { if (i % j == 0) { mark = 0; break; } } if (mark) { printf("%d is a primern", i); } exit(0); //必须退出子进程--否则程序会fork 200次 } } exit(0); } 结果:
zion6135@zion6135-VirtualBox$./primer zion6135@zion6135-VirtualBox$ 30000083 is a primer 30000149 is a primer 30000109 is a primer 30000041 is a primer 30000023 is a primer 30000037 is a primer 30000001 is a primer 30000169 is a primer 30000137 is a primer 30000133 is a primer 30000071 is a primer 30000163 is a primer 30000193 is a primer 30000199 is a primer 30000049 is a primer 30000059 is a primer 30000167 is a primer 30000079 is a primer按理说,200个子线程,相比无fork的一个程序的计算,200个子线程并不一定会更快,因为子线程的切换需要任务调度,开销会更多。
查看程序状态:man ps查看程序状态种类
8.6 wait()和waitpid()之前:
比如parent process有30万条数据,如果希望子进程打印hello world, 那么调用fork()会让子进程复制30万条数据,极大的浪费了资源。
如果用vfork()产生一个子进程,那么子进程和父进程共用一个内存。查看手册:有这么一句话 vfork() was called, or calls any other function before successfully calling _exit(2) or one of the exec(3) family of functions. 这表明在调用vfork()生成的子进程中,只能调用上面的2个函数:exex() 和 _exit(2);
所以如果有这么个问题:父进程打开一个文件描述符,虽然公用一片数据块,但是vfork的子进程是不可以关闭这个文件描述符的。
现在:
fork是父进程,子进程共用一份内存,如果父或子进程需要修改某个数据,那么就会自动的拷贝这一份数据:称为写时拷贝技术。所以现在不怎么需要vfork()函数了.
8.10 函数exec1.wait()
当一个进程正常或者异常终止,就会向它的父进程发送一个SIGCHLD信号。
wait(&status); // 在调用wait后的第一个子进程终止之前,wait() 使其调用者阻塞。
// 等同于waitpid(-1, &status, 0);
wait函数的使用的Demo
#include#include #include #include #include #define LEFT 30000000 #define RIGHT 30000200 int main() { int mark, j; for (int i = LEFT; i <= RIGHT; i++) { pid_t pid = fork(); // 父-->子-->孙-->。。。共200次 if (pid < 0) { perror("error"); exit(1); } if (pid == 0) { mark = 1; for (j = 2; j < i / 2; j++) { if (i % j == 0) { mark = 0; break; } } if (mark) { printf("%d is a primern", i); } exit(0); //必须退出子进程--否则程序会fork 200次 } } // 等子进程结束,主线程为其收尸 for (int i = LEFT; i <= RIGHT; i++) { wait(NULL); //阻塞等待每个子进程exit(0); 退出 }; exit(0); } 结果:
#调用wait的本次打印结果 zion6135@zion6135:~/lbw/gitNote/chap8$ ./a.out 30000001 is a primer ... zion6135@zion6135:~/lbw/gitNote/chap8$ #未调用wait的打印结果 zion6135@zion6135:$ ./a.out zion6135@zion6135:$ 30000083 is a primer 30000149 is a primer ... zion6135@zion6135:$2. waitpid();
pid_t waitpid(pid_t pid, int *wstatus, int options); // 可以控制等待的方式。使其不阻塞。
比如在一个终端bash里面执行一个程序a.out ; 终端bash是aout的父进程,但与调用fork()生成的父子进程不同, 终端bash的子进程是用的exec生成,也是在终端执行 ./a.out这个动作。比如 执行 ./a.out first ---对应到execl函数就是execl(a.out, 1, first);
execl的简单使用+用fflush(null)避免重定向的丢失日志puts("Begin")
#include#include #include int main() { puts("beginn"); // fflush(NULL); int err = execl("/bin/date", "date", "+%s", NULL); perror("execl"); puts("endn"); exit(0); } 打印结果:
abc@123:~/lbw/gitNote/chap8$ ./main begin 1645097863 abc@123:~/lbw/gitNote/chap8$ ./main > tmp abc@123:~/lbw/gitNote/chap8$ cat tmp 1645097919重定向到tmp文件,没有了begin的打印,原因和之前一样,因为execl()函数是替换可执行程序的image, puts("beginn");是行缓冲,如果用execl()函数了,默认变成了全缓冲,所以n不带刷新缓冲的作用了,所以tmp文件中没有Begin的打印日志。所以为了把调用execl()之前的打印放入tmp文件,需要调用fflush(NULL)来刷新缓冲。来让Begin可以重定向到文件tmp中。
虽然execl()函数是替换可执行程序的image,但是程序的pid是没变的。
fork+execl+wait的使用示例:简单的shell模型
#include#include #include int main() { puts("beginn"); fflush(NULL); //避免重定向不能保存begin的日志 pid_t pid = fork(); // 父子进程 if (pid < 0) { perror("fork()"); exit(1); } if (pid == 0) { execl("/bin/date", "date", "+%s", NULL); // 子进程从date文件的main开始执行 perror("execl()"); exit(1); } wait(NULL); //为子进程收尸 puts("endn"); exit(0); } 结果:
123@abc:~/lbw/gitNote/chap8$ ./main begin 1645098780 end 123@abc:~/lbw/gitNote/chap8$比如shell终端的使用例子(shell工作模式)
在shell终端输入ls命令,shell会先fork一个子进程,然后在主进程调用wait(NULL); 等待子进程结束,子进程就取调用execl(“/bin/ls”, "ls" , "%s", null); 去执行可执行文件ls ,然后打印出当前目录下所有文件,子进程结束。主进程的wait(NULL)执行结束。继续打印新的一行!!!!!!
上面讲的过程就是上面写的例子:只不过把父进程换成了shell终端,把执行的文件换成了"ls",而不是"date"......
在比如8.2 fork当中的fork实例2:可以解释为什么结果是先打印了“zion6135@zion6135-VirtualBox$" 后打印了”30000083 is a primer”
因为./primer执行后,shell创建了子进程./primer,./primer fork()结束后,./primer进程在main()函数最后一行调用exit(0)后,shell为其子进程./primer收尸,然后shell打印"zion6135@zion6135-VirtualBox$" , 这个时候./primer的子进程(shell的孙子进程)变成了孤儿进程,继续跑。于是打印了”30000083 is a primer”
为什么父子进程会通用一个shell, 即打印在一个终端里面?
因为父进程fork(),会把自己的文件描述符表复制(dumplicate)给子进程,所以父进程在哪里打印,子进程就在哪里打印。
shell的外部命令简单实现:
外部命令(磁盘上有的程序,ls, cat 等等)
#include#include #include #include #include #define DELINITS " tn" // 三种分割符:空格,tab,换行 struct cmd_st { glob_t globres; }; static void promt() { printf("mysh-0.1$"); } static void prase(const char *buffer, struct cmd_st *cmd) { //解析一行的输入: 比如 ls -a -l /etc /aaa/bbbb char *tok; int i = 0; //第一次不用APPEND, 第二次在APEEND while (1) { tok = strsep(&buffer, DELINITS); if (tok == NULL) { break; } if (tok[0] == ' ') //多个空格 continue; glob(tok, GLOB_NOCHECK | GLOB_APPEND * i, NULL, &cmd->globres); i = 1; } } int main() { puts("beginn"); while (1) { promt(); //打印提示符 char *linebuffer = NULL; size_t line_size = 0; struct cmd_st cmd; //拿到每一行的输入 if (getline(&linebuffer, &line_size, stdin) < 0) { break; } prase(linebuffer, &cmd); //解析命令 if (0) { //内部命令 } else { //外部命令 pid_t pid = fork(); if (pid < 0) { perror("fork()"); exit(1); } if (pid == 0) { execvp(cmd.globres.gl_pathv[0], cmd.globres.gl_pathv); perror("execvp()"); exit(1); } else { wait(NULL); } } } puts("endn"); exit(0); } 结果:
123@abc:~/lbw/gitNote/chap8$ ./main begin mysh-0.1$ls a.out main main.c tmp mysh-0.1$修改/etc/passwd 文件把/bash/bin 改为你写的shell的程序路径,然后切ctrl+alt+f1切换到命令行界面,然后登陆进去,会发现:执行的shell是刚刚写好的shell!!!!!!



