linux进程控制和进程通信
一、进程控制
linux中进程相关及框架
https://blog.csdn.net/happiness_llz/article/details/82345679
单道程序设计
所有进程一个一个排对执行。若A阻塞,B只能等待,即使CPU处于空闲状态。而在人机交互时阻塞的出现时必然的。所有这种模型在系统资源利用上及其不合理,在计算机发展历史上存在不久,大部分便被淘汰了。
多道程序设计
在计算机内存中同时存放几道相互独立的程序,它们在管理程序控制之下,相互穿插的运行。多道程序设计必须有硬件基础作为保证。
时钟中断即为多道程序设计模型的理论基础。 并发时,任意进程在执行期间都不希望放弃cpu。因此系统需要一种强制让进程让出cpu资源的手段。时钟中断有硬件基础作为保障,对进程而言不可抗拒。 操作系统中的中断处理函数,来负责调度程序执行。
在多道程序设计模型中,多个进程轮流使用CPU (分时复用CPU资源)。而当下常见CPU为纳秒级,1秒可以执行大约10亿条指令。由于人眼的反应速度是毫秒级,所以看似同时在运行。
1s = 1000ms, 1ms = 1000us, 1us = 1000ns 1000000000
实质上,并发是宏观并行,微观串行! -----推动了计算机蓬勃发展,将人类引入了多媒体时代。
CPU和MMU(内存管理单元)
我们在程序中用到的所有的内存都是虚拟内存,但是虚拟内存在计算机中是不实际存在的,我们存储的数据都是存储在物理内存中:
进程控制块PCB
我们知道,每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux内核的进程控制块是task_struct结构体。
/usr/src/linux-headers-3.16.0-30/include/linux/sched.h文件中可以查看struct task_struct 结构体定义(不同内核版本不同)。其内部成员有很多,我们重点掌握以下部分即可:
- 进程id。系统中每个进程有唯一的id,在C语言中用pid_t类型表示,其实就是一个非负整数。
- 进程的状态,有初始化、就绪、运行、挂起、停止等状态
- 进程切换时需要保存和恢复的一些CPU寄存器的值
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前工作目录(Current Working Directory)
- umask掩码
- 文件描述符表,包含很多指向已经打开的文件的file结构体的指针的一个数组。 (注:pcb中有一根指针,指针存储的是文件描述符表的首地址)
- 文件描述符表,包含很多指向已经打开的文件的file结构体的指针的一个数组。 (注:pcb中有一根指针,指针存储的是文件描述符表的首地址)
- 和信号相关的信息
- 用户id和组id
进程状态
程基本的状态有5种。分别为初始态,就绪态,运行态,挂起态与终止态。其中初始态为进程准备阶段,常与就绪态结合来看。
ps aux
ps ajx //可以查看父子关系 即进程的pid和它的ppid
进程控制函数
fork()函数用法
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
**函数返回:**失败返回-1;成功返回:① 父进程返回子进程的ID(非负) ②子进程返回 0
详细:https://blog.csdn.net/tomatolee221/article/details/89521054
**注意:**在fork时,对于shell只管fork中的父进程,如果父进程结束shell即会重新输出包含当前用户、路径等的命令行固定格式,但是不会影响子进程,fork的子进程仍可输出,就是可能在终端中看起来比较混乱。
举例1:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
int main()
{
pid_t pid = fork();
int count = 0;
if(pid < 0){
//返回值为负数,调用fork失败
printf("fork failed");
return -1;
}
else if(pid > 0){
//返回值大于0,该值包含新创建子进程的进程ID
printf("我是父进程,进程号为:%d\n", getpid());
count++;
}
else{
//返回值==0,返回到新创建的子进程
printf("我是子进程,进程号为:%d\n", getpid());
count++;
}
printf("输出count的值:%d\n", count);
return 0;
}
循环创建进程
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int n=3;
int i=0;
for(i;i<n;i++)
{
pid=fork();
}
return 0;
}
以上创建了8个进程,进程总数= 2^n其中n是fork系统调用的数量。所以这里n = 3, 2^3 = 8
(新的子进程和原始进程组成)。
创建指定数量的子进程
如果采用以上循环创建的方法,会导致多个分支如下:
以上并不是我们想要的结果,我们是想要由父进程创建多个子进程,我们应该在子进程创建后break
跳出循环,这样子进程就不再调用fork()了
#include <stdio.h>
#include <unistd.h>
int main()
{
pid_t pid = 0;
for(int i = 0;i<3;i++){
pid_t pid = fork();
if(pid == 0)
{
// child
printf("this is child--
>res:%d,PID:%d,PPID:%d\n"
,pid,getpid(),getppid());
break;
//子进程跳出for循环,这样子进程就不再调用fork()了
}
else
{
printf("this is father==>res/child's
pid:%d,PID:%d,PPID:%d\n",
pid,getpid(),getppid());
}
}
return 0;
}
以上代码创建3个子进程,只存在一个父进程,输出结果:
getpid函数
**功能:**获取当前进程ID
pid_t getpid(void);
getppid函数
**功能:**获取当前进程的父进程ID
pid_t getppid(void);
getuid函数
**功能:**获取当前进程的实际用户ID
uid_t getuid(void);
getgid函数
功能:获取当前进程的使用用户组ID
gid_t getgid(void);
exec函数族
fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。
将当前进程的.text、.data替换为所要加载的程序的.text、.data,然后让进程从新的.text第一条指令开始执行,但进程ID不变,换核不换壳。
execlp函数
int execlp(const char *file, const char *arg, ...);//最后参数需要一个NULL结尾
执行程序时,使用path环境变量,执行的程序可以不加路径
参数:
- file 要执行的程序
- arg 参数列表
函数返回:成功:无返回;失败:-1
该函数通常用来调用系统程序。如:ls、date、cp、cat等命令。
举例:
execlp("ls", "ls", "-l", "-F", NULL);
execl函数
int execl(const char *path, const char *arg, ...);
该函数通常用来调用用户自定义的可执行程序
举例:
execl("/bin/ls", "ls", "-l", "-F", NULL);
回收子进程
孤儿进程和僵尸进程
孤儿进程: 父进程先于子进程结束,则子进程成为孤儿进程,子进程的父进程成为init进程,称为init进程领养孤儿进程。(可使用kill -9杀死)
僵尸进程:子进程终止,父进程尚未回收,子进程残留资源(PCB)存放于内核中,变成僵尸(Zombie)进程。
特别注意:僵尸进程是不能使用kill命令清除掉的。因为kill命令只是用来终止进程的,而僵尸进程已经终止。思考!用什么办法可清除掉僵尸进程呢?
方一:wait函数
方二:杀死他的父进程使其变成孤儿进程,进而被系统处理。
wait函数
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的PCB还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用wait或waitpid获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在Shell中用特殊变量$?查看,因为Shell是它的父进程,当它终止时Shell调用wait或waitpid得到它的退出状态同时彻底清除掉这个进程。
父进程调用wait函数可以回收子进程终止信息。该函数有三个功能:
- 阻塞等待子进程退出
- 回收子进程残留资源
- 获取子进程结束状态(退出原因)
pid_t wait(int *status);
参数:传出参数,用来获取子进程退出的状态。
**函数返回:**成功:返回清理掉的子进程ID;失败:-1 (没有子进程)
当进程终止时,操作系统的隐式回收机制会:1.关闭所有文件描述符 2. 释放用户空间分配的内存。内核的PCB仍存在。其中保存该进程的退出状态。(正常终止→退出值;异常终止→终止信号)
可使用wait函数传出参数status来保存进程的退出状态(status只是一个整型变量,不能很精确的描述出状态),因此需要借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
-
WIFEXITED(status) 为非0 → 进程正常结束
WEXITSTATUS(status) 如上宏为真,使用此宏 → 获取进程退出状态 (exit的参数)
-
WIFSIGNALED(status) 为非0 → 进程异常终止(如kill -9等)
WTERMSIG(status) 如上宏为真,使用此宏 → 取得使进程终止的那个信号的编号
-
WIFSTOPPED(status) 为非0 → 进程处于暂停状态
**举例:**如下图
wait的阻塞表现为2秒后才输出“wait ok,…”。
因为wait可以回收子进程的残留资源,如果不加wait,子进程先结束会变为僵尸程序;加上wait后查看则不会。
status输出为102;
waitpid函数
作用同wait,但可指定pid进程清理,可以不阻塞。
pid_t waitpid(pid_t pid, int *status, in options)
参数:
- pid——
- 大于0时,回收指定ID的进程;
- 为0时,回收和当前调用waitpid一个组的所有子进程;
- 为-1时,回收任意子进程(相当于wait);
- 小于 -1 回收指定进程组内的任意子进程(-组ID)
- status 传出参数
- options——为0时,阻塞回收(相当于wait);WNOHANG,费阻塞回收,用轮询结构回收
函数返回:
- 成功:返回清理掉的子进程ID;
- 失败:-1(无子进程);
- 返回0值,第三个参数传WNOHANG,且子进程正在运行。
进程共享
父子进程会进行如下图的共享,非亲缘关系的进程间无法进行以下共享,想要共享的话要通过进程通信。
父子进程之间在fork后的异同:
刚fork之后:
父子相同处(0-3G的用户区及3-4G的内核区大部分): 全局变量、.data、.text、栈、堆、环境变量、用户ID、宿主目录、进程工作目录、信号处理方式…
父子不同处(3-4G中的内核区的PCB区): 1.进程PID(在PCB中) 2.fork返回值 3.父进程ID 4.进程运行时间 5.闹钟(定时器)(定时器是以进程为单位进行分配,每个进程有且仅有一个) 6.未决信号集
似乎,子进程复制了父进程0-3G用户空间内容,以及父进程的PCB,但pid不同。真的每fork一个子进程都要将父进程的0-3G地址空间完全拷贝一份,然后在映射至物理内存吗?
当然不是!父子进程间遵循读时共享写时复制的原则(针对的是物理地址)。这样设计,无论子进程执行父进程的逻辑还是执行自己的逻辑都能节省内存开销。
二、进程通信
概述
进程通信和线程通信
- 进程通信:在用户空间实现进程通信是不可能的,通过Linux内核通信
- 线程通信:可以在用户空间就可以实现,可以通过全局变量通信。
进程通信方式
进程通信方式都是基于文件IO的思想
- 管道通信:无名管道、有名管道
- 信号通信:信号(通知)通信包括:信号的发送、信号的接收和信号的处理。
- IPC(Inter-Process Communication)通信:共享内存、消息队列和信号灯
以上是单机模式下的进程通信(只有一个 Linux内核)
- Socket通信:存在于一个网络中两个进程之间的通信(两个Linux内核)。
1.无名管道(管道)
https://blog.csdn.net/c15522627353/article/details/52972941
无名管道通信原理
管道文件是一个特殊的文件,类似一个顺序队列,属于半双工通信
无名管道相关函数
pipe
pipe函数创建无名管道
#include <unistd.h>
int pipe(int pipefd[2]);
**功能:**创建管道,为系统调用
参数:
- 就是得到的文件描述符。可见有两个文件描述符:pipefd[0]和pipefd[1],管道有一个读端fd[0]用来读和一个写端fd[1]用来写,这个规定不能变。
**函数返回值:**成功是0,出错是-1;
具体使用举例:
例1:
#include <unistd.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main()
{
int fd[2];
int ret;
char writebuf[]="hello linux";
char readbuf[]={
0};
ret=pipe(fd);
if(ret<0)
{
printf("create pipe failure\n");
return -1;
}
printf("create pipe sucess fd[0]=%d,fd[1]=%d\n",fd[0],fd[1]);
write(fd[1],writebuf,sizeof(writebuf));
//start read from pipe
read(fd[0],readbuf,128);
printf("readbuf=%s\n",readbuf);
//second read from pipe
memset(readbuf,0,128);
read(fd[0],readbuf,128);
printf("second read readbuf=%s\n",readbuf);
close(fd[0]);
close(fd[1]);
return 0;
}
输出结果:
可见第一次读写都正常,但第二次从管道读时出现阻塞。
例2:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
int main()
{
pid_t pid;
int fd[2];
int ret;
int process_inter=0;
ret=pipe(fd);
if(ret<0)
{
printf("create pipe failure\n");
return -1;
}
printf("create pipe sucess\n");
pid=fork();
if(pid==0) //chlid process
{
int i=0;
read(fd[0],&process_inter,1);
while(process_inter==0); //process_inter=1 exit
{
for(i=0;i<5;i++)
{
printf("this is child process i=%d\n",i);
usleep(100);
}
}
}
if(pid>0)
{
int i=0;
for(i;i<5;i++)
{
printf("this is parent process i=%d\n",i);
usleep