进程共享(父子进程虚拟地址空间情况)
fork之后父子进程的异同:
相同:全局变量、data、bss、.txt、堆栈、环境变量、用户ID、当前工作目录.....
不同:进程ID fork的返回值 父进程ID 进程运行的时间 定时器 未决信号集
似乎子进程复制了父进程0~3G用户空间的内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要讲父进程0-3G地址完全拷贝一份吗?然后再映射至物理内存吗?
前面说到,虚拟地址空间并不是说真的需要4G的空间,往往只需要实现逻辑结构的页表、页目可以了。而且父子进程间遵循读时共享写时复制的原则。无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。(如果只有读操作==>父子进程共享一块物理内存,只要有了写操作==>进行复制物理内存)
在这里我们一定要分清楚虚拟地址空间和物理内存,虚拟地址空间相当于在程序与物理内存之间的中间层。
父子进程共享:文件描述符表 mmap建立的映射区(重点)
GBD多进程调试(重点)
使用GDB调试的时候,gdb只能跟踪一个进程。可以在fork之前,通过指令设置gdb调试工具跟踪父进程还是子进程。默认跟踪父进程
set follow-fork-mode child
set follow-fork-mode parent
如果有多个子进程怎么处理呢?
使用gdb条件调试:b if i = 2 set follow-fork-mode child
默认情况下,当调试一个进程时,其他的进程也在运行。
如果想在调试某一个进程的时候,其他进程被挂起可以设置调试模式:
set detach-on-fork on/off
查看调试的进程:info inferiors
切换当前调试的进程: inferior id
使进程脱离GDB调试:defach inferiors id
exec函数族(重点掌握两个就可以了)
fork创建子进程后执行的是父进程相同的程序(但有可能执行不同的代码分支)。
通过exec函数可以使子进程执行另一个程序。
原理:当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行(你就理解位从main函数执行就可以了)。调用exec函数不创建新进程,只是进程代码和数据的替换,故进程id不会发生改变。
int execl(const char *path,const char *arg,...);
int execlp(const char *file,const char *arg,...); //环境变量中有
int execle(const char *path,const char *arg,...,char *const envp[]);
int execv(const char *path,char *const argv[]);
int execvp(const char *file,char *const argv[]);
int execve(const char *path,char *argv[],char *const envp[]);
#include <iostream> #include <unistd.h> using namespace std; int main(void) { pid_t pid = fork(); if(pid == -1) { perror("fork"); exit(-1); } if(pid == 0) { execl("/bin/ls","ls","-l",NULL); } else { cout<<"草泥马..."<<endl; } return 0; } #include <iostream> #include <unistd.h> using namespace std; int main(void) { pid_t pid = fork(); if(pid == -1) { perror("fork"); exit(-1); } if(pid == 0) { execlp("ls","ls","-l",NULL); } else { cout<<"草泥马..."<<endl; } return 0; }
#include <iostream> #include <unistd.h> using namespace std; int main(void) { pid_t pid = fork(); if(pid == -1) { perror("fork"); exit(-1); } if(pid == 0) { execl("./a","a",NULL); } else { cout<<"草泥马..."<<endl; } return 0; } #include <iostream> using namespace std; int main(void) { cout<<"我是你大爷...."<<endl; return 0; }
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> using namespace std; int main(void) { int fd; fd = open("ps.out",O_RDWR|O_CREAT,0664); dup2(fd,STDOUT_FILENO); execlp("ps","ps","aux",NULL); return 0; }
回收子进程
孤儿进程
父进程先于子进程结束,则子进程成为孤儿进程,会被进程孤儿院收养。
进程孤儿院:init (1号进程)
孤儿进程不会有影响,安全
说一下init进程:
init进程会循环的wait()它的已经退出的子进程。这样当一个孤儿进程凄凉的结束了,init进程就会代表党和政府处理一切善后工作。
僵尸进程
进程终止,父进程尚未回收,子进程残留资源仍然存放在内核中,变成僵尸进程。【 每一个进程结束之后,都会释放自己的用户区,内核的部分。但内核中PCB没有办 法自己释放掉,需要父进程区释放。进程终止时,父进程尚未回收,子进程残留资源存在内核中,成为僵尸进程。如此以来就会导致一个问题,如果父进程不调用wait()或waitpid()的话,那么保留的那段信息将不会被释放掉。那么该进程的进程号就一直处于被占用状态,但是系统能够提供的进程号数量是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致不能产生新的进程】
如何解决处理僵尸进程:
1.让僵尸进程变成孤儿进程,杀死其父进程就可以了。那么这个僵尸进程就会被init收养,init内部调用了wait函数来回收子进程 kill -9 父进程ID
2.父进程通过wait或者waitpid函数回收子进程
wait()和waitpid函数
一个进程在终止时会关闭所有的文件描述符,释放在用户空间分配的内存,但它的PCB中还保留着,内核在其中保留了一些信息。如果是正常退出保存退出状态,如果是异常终止则保留着导致异常终止的信号。其父进程可以调用wait和waitpid获取这些信息,然后彻底清除。
pid_t wait(int *status);
成功:清除掉的子进程ID 失败:-1
功能:
阻塞等待子进程退出
回收子进程残留资源
获取子进程结束状态(退出状态)
如何获取退出状态(系统定义的宏,调用这些宏就可以了):
1.WIFEXITED(status) 为非0 --> 进程正常退出
WEXITSTATUS(status) --> 获取进程退出状态
2.WIFSIGNALED(status) 为非0 --> 进程异常终止
WTERMSIG(status) --> 获取导致进程终止的信号编号
3.WIFSTOPPED(status) --> 进程处于暂停状态
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; int main(void) { pid_t pid = fork(); if(pid == -1) { perror("fork"); exit(-1); } if(pid > 0) { cout<<"我是你爹爹"<<endl; int status; int res = wait(&status); //阻塞等待 if(WIFEXITED(status)) { cout<<WEXITSTATUS(status)<<endl; } } else if(pid == 0) { cout<<"我是你崽...wuwu"<<endl; exit(-3); } return 0; }
通过信号来终止子进程
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; int main(void) { pid_t pid = fork(); if(pid == -1) { perror("fork"); exit(-1); } if(pid > 0) { cout<<"我是你爹爹"<<endl; int status; int res = wait(&status); //阻塞等待 if(WIFSIGNALED(status)) { cout<<WTERMSIG(status)<<endl; } } else if(pid == 0) { cout<<getpid()<<endl; while(1) { sleep(2); cout<<"我是你崽...wuwu"<<endl; } } return 0; }
waitpid函数
wait()和waitpid()函数的功能一样,区别在于wait()函数会阻塞,waitpid()可以设置不阻塞,waitpid()还可以指定等待哪个子进程结束。
一次wait和waitpid调用只能清理一个子进程,清理多个子进程应使用循环
pid_t waitpid(pid_t pid,int *status,int options)
参3:
0 阻塞
WNOHANG 非阻塞
进程退出
#include <stdlib.h>
void exit(int status);
------------------------------------------
#include <unistd.h>
void _exit(int status);
进程间通信(IPC方法)
基本概念:
进程是一个独立的资源分配单元(进程使操作系统分配资源的基本单位),不同进程之间的资源使独立的,没有联系的,不能在一个进程中直接访问另一个进程的资源。但是,进程不是孤立存在的,不同的进程需要进行信息的交互和状态的传递等,因此需要进程间通信。
进程间通信的目的:
数据传输:一个进程需要将它的数据发送给另一个进程
通知事件:一个进程需要向另一个或一组进程发消息,
通知它发生了某种事程(如进程终止要通知给父进程)
资源共享:多个进程之间共享同样的资源
进程控制:一些进程希望完全控制另一个进程的执行(DEBUG)
Linux进程间通信的方式
同一主机:管道(有名管道、匿名管道) 信号 消息队列 共享内存 内存映射 本地套接字
不同主机:socket套接字
进程间通信的基础
所有进程的内核空间所映射的物理内存是同一块
匿名管道
我们通常称的管道就是匿名管道,它是UNIX系统IPC(进程间通信)的最古老的方式,所有UNIX系统都支持这种通信机制。
统计一个目录中文件的数目命令:
ls | wc -l (ls和wc为两个不同的进程 |是管道符)
管道的特点
管道其实是一个在内核中维护的缓冲区,这个缓冲区的存储能力是有限的。ulimit -a
管道拥有文件的特质,读操作和写操作,匿名管道没有文件实体(伪函数),有名管道有文件实体,但不存储数据。可以按照操作文件的方式对管道进行操作。
一个管道是一个字节流,使用管道是不存在消息或消息边界的概念。
通过管道传递的数据是顺序的,从管道中读取出来的字节顺序和它们被写入管道的顺序是完全一样的。
管道是半双工的
匿名管道只能在具有公共祖先的进程之间使用
管道的数据结构
循环队列
管道的使用
int pipe(int pipefd[2]);
功能:创建一个管道
参数:int pipefd[2]这个数组是一个传出参数
pipefd[0] 对应的是管道的读端
pipefd[1] 对应的是管道的写端
返回值:
成功 0
失败 -1
注意:匿名管道只能用于具有关系的进程之间的通信(父子,兄弟)
管道默认是阻塞的,如果管道中没有数据,read阻塞,如果管道满了,write阻塞
管道创建成功之后,创建管道的进程拥有管道的读端和写端....父子进程之间想要通信
看管道缓冲区的大小
ulimit -a
long fpathconf(int fd,int name)
#include <iostream> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> using namespace std; int main(void) { //创建管道 int pipefd[2]; int ret = 0; ret = pipe(pipefd); if(ret == -1) { perror("pipe"); exit(-1); } //fork子进程 pid_t pid = fork(); if(pid == -1) { perror("fork"); exit(-2); } if(pid > 0) { char buf[256] = "aaaaa"; close(pipefd[0]); //关闭读端 write(pipefd[1],buf,sizeof(buf)); int status; wait(&status); } else if(pid == 0) { close(pipefd[1]); //关闭写端 char buf[256]; while(1) { int ret = read(pipefd[0],buf,sizeof(buf)); if(ret == -1) { perror("read"); exit(-3); } if(ret == 0) { break; } cout<<buf<<endl; } } return 0; }
管道读写行为
读管道:
1.管道中有数据,read返回实际读到的字节数
2.管道中无数据:
管道写端被全部关闭,read返回0
管道写端没有全部关闭,read阻塞等待
写管道:
1.管道读端全部关闭,进程异常终止(SIGPIPE)
2.管道读端没有全部关闭
管道已满,write阻塞
管道未满,write将数据写入,返回实际写入到字节数
管道的优缺点
优点:简单
缺点:只能单向通信,双向通信需要建立两个管道
只能用于有血缘关系的进程间