ch3进程学习笔记
知识点归纳
进程是对映像的执行
系统资源包括内存空间、I/O设备、CPU
PCB进程控制块,PROC结构体
简易的PROC:
typedef struct proc{
struct proc *next; // next proc pointer
int *ksp; // saved sp: at byte offset 4
int pid; // process ID
int ppid; // parent process pid
int status; // PROC status=FREE|READY, etc.
int priority; // scheduling priority
int kstack[1024]; // process execution stack
}PROC;
- ksp 保存堆栈指针,以便进程恢复。当进程放弃CPU时,会将上下文保存在堆栈中。
- pdi 进程id编号
- ppid 父进程id编号
- status 进程当前状态
- priority 进程调度优先级
- kstack[1024]进程执行时的堆栈
睡眠模式,应该就是阻塞
进程被阻塞之后,放弃CPU,转交给进程调度,自己等待资源满足后被唤醒
唤醒操作
被唤醒的进程只是进入就绪状态,进入就绪队列,是否运行还要看进程调度
fork函数(最有收获)
返回值:子进程中返回0 ,父进程中返回子进程id,错误返回-1
fork()之前的代码,子进程无法执行。
fork()之后的代码,子进程有,并且可以执行。
既然如此,那子进程和父进程在fork()之后的行为,岂不是一模一样?
没错,相当于一份代码,控制两个不同进程,进行不同的行为,所以需要用到分支语句,再加上fork()函数在两个进程中不同的返回值,让父子进程进入相同代码的不同分支中执行。
那如果要创建多个子进程呢?
首先,比如要创建5个子进程,直接写一个五次的循环是不行的,因为子进程也会执行父进程fork后面代码,5次循环将会有2^5=32个进程
所以我们在子进程中加break,让子进程不再创建孙子进程
同时,由于break后,i不再增加,所以一个i值对应一个子进程,甚是巧妙!
这样,后面就可以使用i值作为分支条件,分别控制5个进程。
(具体实现见最后的实践内容)
而且父子进程共用一个文件描述符表,所以子进程的标准输入、输出、错误,都默认和父进程一样。
如果父进程先于子进程终止,孤儿进程会被孤儿院回收(用户/内核init进程)
wait和waitpid
wait:回收子进程资源,阻塞回收任意一个。
pid_t wait(int *status)
参数 status(传出)回收进程的状态。
返回值。成功。回收进程的pid
失败。-1,errno
阻塞等待子进程退出、清理子进程残留在内核的 pcb资源,通过传出参数,得到子进程结束状态
获取子进程正常终止值:
WIFEXITED(status)--》为真--》调用EXITSTATUS(status)--》得到子进程退出值。
获取导致子进程异常终止信号。
WIFSIGNALED(statusm) --》为真--》调用WTERMSIG(status) --》得到导致子进程异常终止的信号编号。
waitpid函数。指定某一个进程进行回收。可以设置非阻塞。
pid_t waitpid(pid_t pid ,int *status,int options)
参数
pid指定回收的子进程
pid>0 : 待回收的子进程id
pid=-1: 任意子进程
pid=00:同组的子进程。
options:WNOHANG指定回收方式为非阻塞。
exec函数族
该函数成功时没有返回值,调用成功后,进程就去执行另外一个程序
所以,写在exec函数后面的代码,只有在exec出错时,才有可能执行
原来进程的fork出来的代码段、数据段、堆栈等,都将被替换成新的程序的内容。但pid不变
环境变量是为当前sh定义的变量,他们定义了程序的执行环境。
使用 env 查看环境变量,export修改环境变量
管道和命名管道
管道是用于进程交换数据的单向进程间通信通道,管道有一个读取端和一个写入端。
命名管道是不相关进程间的FIFO通信通道。
读取和写入管道通常是同步、阻塞操作。
阻塞:
读数据时:管道里没有数据,但是还有写入进程
写数据时:管道里没有存储空间,但还有读出进程
错误:
读数据时:管道里没有数据,而且没有写入进程
写数据时:管道没有读出进程
命名管道是伪文件,本身不占用任何空间。是文件系统的一部分,并按目录进行组织。
使用库函数mkfifo()或者系统调用mknod(X,S_IFIFO,X)创建命名管道
问题与解决思路
如何gdb调试带fork的程序?
gdb默认进入fork的父进程
可以使用命令 set follow-fork-mode child 进入使其进入子进程
如果使用默认设置,gdb会先将fork出的子进程执行完毕,再接着执行父进程。
云班课答疑区
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4
5 int main(){
6 int i;
7 for(i=0;i<2;i++){
8 fork();
9 printf("-");
10 }
11 return 0;
12 }
这个程序的输出结果是8个'-',即 --------,刚开始确实令人摸不到头脑。
第一次fork后,有2个进程,分别打印1个'-',共2个
第二次fork后,有4个进程,分别打印1个'-',共4个
一共应该只打印6个才对。
后来将代码改成printf("-\n"),就正常打印了6个
于是怀疑跟stdout的行缓冲有关。fork出来的子进程把缓冲区也复制了。
第一次fork后,父进程缓冲区里有1个,大儿子缓冲区里有1个
第二次fork后,printf之前,父进程缓冲区里有1个,大儿子缓冲区里有1个,二儿子复制了父进程缓冲区里的1个,孙子复制了大儿子缓冲区里的1个
第二次fork后,printf之后,四个进程缓冲区都增加1个,共增加4个
这样一共就会打印8个
用gdb调试一下,验证猜想
到这里,大儿子和孙子进程执行结束,将缓冲区的内容打印出来,一共打印4个,符合猜想
到这里,二儿子进程执行结束,将缓冲区的内容打印出来,一共打印2个,符合猜想
最后父进程执行结束,程序退出,将缓冲区的内容打印出来,一共打印2个,符合猜想
实践内容
fork
还有getpid()和getppid()
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <pthread.h>
5
6 int main(){
7 printf("before fork 1\n");
8 printf("before fork 2\n");
9 printf("before fork 3\n");
10
11 pid_t pid = fork();
12 if(pid == -1){
13 perror("fork error");
14 exit(1);
15 }else if(pid ==0 ){
16 printf("---child : my id = %d , myparent id = %d\n",getpid(),getppid());
17 }else if(pid >0){
18 printf("---parent : my child id = %d,my id = %d , my parent id = %d\n",pid,getpid(),getppid());
19 }
20 printf("=========EOF==========\n");
21 return 0;
22 }
nfork
循环创建多个子进程
孤儿进程被孤儿院回收了,所以ppid = 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <pthread.h>
5
6 int main(){
7 int i;
8 for(i=0;i<5;i++){
9 if(fork()==0) break;
10 }
11 if(i==0){
12 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
13 }else if(i==1){
14 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
15 }
16 else if(i==2){
17 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
18 }
19 else if(i==3){
20 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
21 }
22 else if(i==4){
23 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
24 }else if(i==5){
25 printf("I'm parent,my id = %d\n",getpid());
26 }
27 return 0;
28 }
产生孤儿进程的原因就是子进程没有抢过父进程,父进程拿到CPU之后直接全部执行完事了。
解决:用sleep阻塞一下父进程即可。
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <pthread.h>
5
6 int main(){
7 int i;
8 for(i=0;i<5;i++){
9 if(fork()==0) break;
10 }
11 if(i==0){
12 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
13 }else if(i==1){
14 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
15 }
16 else if(i==2){
17 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
18 }
19 else if(i==3){
20 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
21 }
22 else if(i==4){
23 printf("I'm %dth child,my id = %d,my parent id = %d\n",i+1,getpid(),getppid());
24 }else if(i==5){
25 sleep(1);
26 printf("I'm parent,my id = %d\n",getpid());
27 }
28 return 0;
29 }
execlp
默认在/bin/寻找可执行文件
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5
6 int main(){
7 pid_t pid=fork();
8 if(pid == -1){
9 perror("fork error");
10 exit(1);
11 }else if(pid == 0){
12 //child
13 execlp("ls","ls","-l",NULL);
14 perror("exec error");
15 exit(1);
16 }else if(pid > 0){
17 //parent
18 sleep(1);
19 printf("I'm parent : %d\n",getpid());
20 }
21 return 0;
22 }
execl
将可执行文件的路径作为第一个参数传入
使用dup将执行结果重定向到./result.txt
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <pthread.h>
4 #include <unistd.h>
5 #include <fcntl.h>
6
7 int main(){
8 pid_t pid=fork();
9 if(pid == -1){
10 perror("fork error");
11 exit(1);
12 }else if(pid == 0){
13 //child
14 int fd = open("result.txt",O_WRONLY|O_CREAT|O_TRUNC,0644);
15 if(fd<0){
16 perror("open file error");
17 exit(1);
18 }
19 dup2(fd,STDOUT_FILENO);
20 execl("/bin/date","date",NULL);
21 perror("exec error");
22 exit(1);
23 }else if(pid > 0){
24 //parent
25 sleep(1);
26 printf("I'm parent : %d\n",getpid());
27 }
28 return 0;
29 }
wait
以及相关的宏函数
2 #include <stdlib.h>
3 #include <unistd.h>
4 #include <sys/wait.h>
5
6 int main(){
7 pid_t pid;
8
9 pid = fork();
10 if(pid==0){
11 //child
12 printf("child id = %d\n",getpid());
13 sleep(10);
14 printf("----------child die-----------\n");
15 return 37;
16 }else if(pid > 0){
17 //parent
18 int status;
19 pid_t wpid = wait(&status);
20 if(wpid==-1){
21 perror("wait error");
22 exit(1);
23 }
24 if(WIFEXITED(status)){
25 //normally exit
26 printf("child normally exit with %d\n",WEXITSTATUS(status));
27 }else if(WIFSIGNALED(status)){
28 //kill by signal
29 printf("child is killed by signal %d\n",WTERMSIG(status));
30 }
31 printf("---------parent wait child %d finish--------\n",wpid);
32 }else{
33 perror("fork error");
34 }
35
36 return 0;
37 }
waitpid
用waitpid以非阻塞方式,回收多个进程中指定的子进程
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
int main()
{
int i;
pid_t children[5];
for(i=0;i<5;i++){
pid_t pid = fork();
if(pid==0){
break;
}else if(pid>0){
children[i] = pid;
}
}
if(i==5){
//parent
for(int j=0;j<5;j++){
while(waitpid(children[j],NULL,WNOHANG)!=-1);
printf("parent has waited child id = %d\n",children[j]);
}
}else{
//children
printf("I'm %dth child,id = %d\n",i,getpid());
}
return 0;
}
pipe函数
用pipe实现man -k dir | grep 2
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <stdlib.h>
5
6 int main()
7 {
8 int fd[2];
9 if(pipe(fd)==-1){
10 perror("pipe create error");
11 exit(1);
12 }
13
14 pid_t pid = fork();
15 if(pid == -1){
16 perror("fork error");
17 exit(1);
18 }else if(pid == 0){
19 //child execute man -k
20 close(fd[0]); //向管道写入数据,关闭管道的读端
21 dup2(fd[1],STDOUT_FILENO);
22 execlp("man","man","-k","dir",NULL);
23 perror("exec man -k error");
24 exit(1);
25 }else if(pid > 0){
26 //parent execute grep
27 close(fd[1]); //从管道读出数据,关闭管道的写端
28 dup2(fd[0],STDIN_FILENO);
29 execlp("grep","grep","2",NULL);
30 perror("exec man -k error");
31 exit(1);
32 }
33 return 0;
34 }
兄弟进程间使用pipe管道通信
依旧是实现 man -k dir | grep 2
注意关闭父进程的管道的两端
1 #include <stdio.h>
2 #include <unistd.h>
3 #include <sys/types.h>
4 #include <stdlib.h>
5 #include <sys/wait.h>
6 int main(){
7 int fd[2];
8 if(pipe(fd)==-1){
9 perror("pipe create error");
10 exit(1);
11 }
12 int i;
13 for(i=0;i<2;i++){
14 if(fork()==0) break;
15 }
16 if(i == 2){
17 //parent 既不读,也不写,pipe两端全关上
18 close(fd[0]);
19 close(fd[1]);
20 wait(NULL);
21 wait(NULL);
22 }else if(i == 0){
23 //child 0 execute man -k
24 close(fd[0]); //向管道写入数据,关闭管道的读端
25 dup2(fd[1],STDOUT_FILENO);
26 execlp("man","man","-k","dir",NULL);
27 perror("exec man -k error");
28 exit(1);
29 }else if(i == 1){
30 //child 1 execute grep
31 close(fd[1]); //从管道读出数据,关闭管道的写端
32 dup2(fd[0],STDIN_FILENO);
33 execlp("grep","grep","2",NULL);
34 perror("exec man -k error");
35 exit(1);
36 }
37 return 0;
38 }
命名管道
mkfifo函数
两个进程,分别进行读写操作
mkfifo
2 #include <sys/stat.h>
3 #include <stdio.h>
4 int main(){
5 if(mkfifo("myfifo",0664)==-1){
6 perror("mkfifo error");
7 }
8 return 0;
9 }
读写fifo操作就是文件读写操作,就不上代码了
使用mmap进行进程通信
mmap是一种内存映射文件的方法,即将一个文件或者其它对象映射到进程的地址空间,实现文件磁盘地址和进程虚拟地址空间中一段虚拟地址的一一对映关系。实现这样的映射关系后,进程就可以采用指针的方式读写操作这一段内存,而系统会自动回写脏页面到对应的文件磁盘上,即完成了对文件的操作而不必再调用read,write等系统调用函数。相反,内核空间对这段区域的修改也直接反映用户空间,从而可以实现不同进程间的文件共享。
有血缘关系
3 #include <sys/mman.h>
4 #include <string.h>
5 #include <sys/wait.h>
6 #include <stdlib.h>
7 #include <fcntl.h>
8
9 int main(){
10 int fd = open("1.txt",O_RDWR | O_CREAT | O_TRUNC,0644);
11 if(fd<0) return 1;
12
13 ftruncate(fd,32);
14
15 char *p = (char*)mmap(NULL,32,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
16 if(p ==MAP_FAILED){
17 perror("mmap error");
18 exit(1);
19 }
20 close(fd);
21
22 if(fork()==0){
23 strcpy(p,"hello parent");
24 }else{
25 printf("parent reading:\n%s\n",p);
26 }
27 munmap(p,32);
28 return 0;
29 }
有血缘关系也可以用匿名映射,MAP_ANONYMOUS
无血缘关系和fifo的编程思路一致,两个进程用mmap映射同一个文件即可。