文章目录
1.进程和程序
1.1进程和程序的概念
程序的概念:程序是一种文件,编译好的二进制文件
进程:运行中的程序。
站在程序员的角度,是运行一系列指令的过程
站在操作系统的角度:分配系统资源的基本单位
区别:程序占用的是磁盘资源,不占用系统的资源
内存占用系统资源
一个程序可以对应多个进程,而一个进程对应一个程序。
因此程序没有生命周期,但是进程有生命周期。
1.2单道和多道程序设计
单道程序设计:一次只有一个处于程序在运行中;其他程序处于等待状态。(DOS)
多道程序设计:设计出时间片,在一个时间片中执行一个程序,在下一个时间片立马然后切换为下一个程序,从而让出cpu的资源给其他程序。
由于一个时间片的时间很短,属于是毫秒级别的。所以在人的感知上,几个程序是并发进行的;但是在微观上,在一个时间片上,只有一个程序在运行。微观上串行,宏观上并行
1.3进程状态的转换
cpu和MMU(内存管理单元)
- 寄存器的运算速度最快
- 算术逻辑单元只能继续+计算和<<运算
1.3.1进程的状态切换
1.3.2MMU(内存管理单元的作用)
**在32位机器下,一个进程可以管理的虚拟内存空间大小为4G。**而实际上的物理空间大小不是4G。而物理内存和虚拟内存自己的对应和管理就是通过MMU和地址转换记录表进行。
比如在.data中存放了a=10,那么就可以通过MMU(记录了虚拟内存和物理内存的映射关系),将他的虚拟地址转换为物理地址,从而访问内容10。
内存访问级别:
0是最高的级别,内核访问的权限
3是允许用户访问权限级别的权限
映射问题:
用户空间映射到物理空间内存是独立的(不同的进程,即使虚拟地址相同,对应的也是不同的物理空间)
内核空间映射到物理空间内存是相同的。(上图中,两个进程的kernal指向了同一块权限为0的MMU)
1.3.4PCB(进程控制块)的认识
每一个进程在内核中都有一个进程控制块PCB(如上图)来维护进程的相关信息,在linux下内核的进程控制块是struct task_struct 结构体。
task_struct的重点成员解析:
- 进程id,系统中每一个进程有唯一的id,在结构体中用pid_t类型表示,本质上是一个非负整数。(方便进程的管理)
- 进程的状态:就绪,运行,挂起,停止
- 进程切换时需要保存和恢复的一些CPU寄存器
- 描述虚拟地址空间的信息
- 描述控制终端的信息
- 当前的工作目录
- umask掩码
- 文件描述符表,包含很多指向file结构体的指针
- 和信号相关的信息
- 用户id和组id
- 会话和进程组
- 进程可以使用的资源上限
1.3.5获取环境变量
env #获取所有环境变量
#常用的环境变量:PATH,SHELL,TERM,LANG,HOME
PATH:可执行文件的搜索路径
SHELL:显示当前SHELL
echo $TERM:当前的终端类型,在图形界面终端下值通常为xterm,
echo $LANG:语言和locale,决定了字符串编码以及时间、货币等信息的显示格式
echo $HOME:当前用户的主目录路径,很多程序需要在自己的主目录下保存配置文件,使得每个用户在执行该程序时都有自己的一套配置。
getenv获取环境变量函数
//获取环境变量
char* getenv(const char* name);
/*
成功:返回环境变量的值,失败:返回NULL
*/
mygetenv.c
int main(){
const char*s="PATH";
char*print=getenv(s);
prinf("%s\n",print);
return 0;
}
gcc mygetenv.c
./a.out
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin
setenv设置环境变量函数
int setenv(const char*name,const char*value,int overwrite);
/*
参数overwrite取值: 1.覆盖原环境变量
0.不覆盖,常用于设置新环境变量
*/
/*
使用指令的方式添加环境变量
key为环境变量类型,value为环境变量取值
*/
export key=value
unsetenv删除环境变量函数
int unsetenv(const char*name);
/*
成功:0
失败:-1
name不存在:0
*/
2.控制进程
2.1进程控制函数fork
fork()函数通过系统调用创建一个与原来进程几乎完全相同的进程,也就是两个进程可以做完全相同的事,但如果初始参数或者传入的变量不同,两个进程也可以做不同的事。
#include<unistd.h>
pid_t fork(void);
/*
pid_t是进程的ID
失败:返回-1
返回值,成功:有两次返回;父进程返回子进程的ID
子进程返回0;
*/
//进程获取函数
#include<sys/types.h>
#include<unistd.h>
//获得当前进程的ID
pid_t getpid(void);
//获得当前进程父进程的ID
pid_t getppid(void);
#include<stdio.h>
#include<unistd.h>
int main(){
printf("begin.....\n");
pid_t pid=fork();
printf("end.......\n");
return 0;
}
printf(“end…\n”);执行了两次,声明fork()执行之前只有一个进程,fork()执行之后出现了两个进程。
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
int main(){
printf("begin()...\n");
pid_t pid=fork();
//如果小于0,表示进程打开失败
if(pid<0){
perror("fork err");
exit(-1);
}
if(pid==0){
printf("I am child pid,pid=%d,ppid=%d",getpid(),getppid());
}
else if(pid>0){
printf("I am father pid,pid=%d,child=%d,ppid=%d",getpid(),pid,getppid());
}
printf("end......\n");
return 0;
}
/*
解释pid的值为什么在父子进程中不同。
其实就相当于链表,进程形成了链表,父进程的pid(p意味point)指向子进程的进程id, 因为子进程没有子进程,所以其pid为0。这里也解释了为什么父进程fork返回的是子进程的id,而子进程返回的是0;
可以证明的是:观察上面的父子进程id,父进程的id刚好比子进程少1
*/
分析:
进程从fork()函数执行后开始变成两个进程,在linux下,子进程和父进程是并发进行的。当然我们应该让子进程先结束,然后父进程再结束,否则就会出现孤儿进程。
int main(){
printf("begin()...\n");
pid_t pid=fork();
//如果小于0,表示进程打开失败
if(pid<0){
perror("fork err");
exit(-1);
}
if(pid==0){
printf("I am child pid,pid=%d,ppid=%d",getpid(),getppid());
}
else if(pid>0){
printf("I am father pid,pid=%d,child=%d,ppid=%d",getpid(),pid,getppid());
sleep(1);
}
printf("end......\n");
return 0;
}
延缓父进程的结束时间,让子进程先结束。
2.1.2fork()函数进阶
//先看一份代码
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(void)
{
int i=0;
printf("i son/pa ppid pid cpid\n");
//ppid指当前进程的父进程pid
//pid指当前进程的pid,
//fpid指fork返回给当前进程的值
for(i=0;i<2;i++){
pid_t cpid=fork();
if(fpid==0)
printf("%d child %4d %4d %4d\n",i,getppid(),getpid(),cpid);
else
printf("%d parent %4d %4d %4d\n",i,getppid(),getpid(),cpid);
}
return 0;
}
执行顺序
for i=0 1
father father
son
son father
son
//问题是下面的进程一个创建了多少个进程(除去main进程)
#include <stdio.h>
#include <unistd.h>
int main(int argc, char* argv[])
{
fork();
fork() && fork() || fork();
fork();
return 0;
}
//一共有20个进程,除去main还有19个进程。
A&&B,如果A=0,就没有必要继续执行&&B了;A非0,就需要继续执行&&B
A||B,如果A非0,就没有必要继续执行||B了,A=0,就需要继续执行||B。
2.2进程命令控制
int main(){
printf("begin()...\n");
pid_t pid=fork();
//如果小于0,表示进程打开失败
if(pid<0){
perror("fork err");
exit(-1);
}
if(pid==0){
printf("I am child pid,pid=%d,ppid=%d",getpid(),getppid());
while(1){
printf("I am child\n");
sleep(1);
}
}
else if(pid>0){
printf("I am father pid,pid=%d,child=%d,ppid=%d",getpid(),pid,getppid());
while(1){
printf("I am father\n");
sleep(1);
}
}
printf("end......\n");
return 0;
}
可以看出子进程和父进程是并发进行的;
ps aux #查看进程信息
ps ajx #查看进程和进程的血缘关系
kill -9 pid #杀死进程ID为pid的进程 -9表示强制杀死进程
kill -9 11549
2.3创建n个进程
让父进程创建多个子进程
int main(){
int n=5;
int i=0;
pid_t pid=0;
for(i;i<n;i++){
pid=fork();
if(pid==0){
printf("I am child,pid=%d,ppid=%d",getpid(),getppid());
break;//子进程跳出循环,不再创建子进程
}
else{
printf("I am father,pid=%d,ppid=%d",getpid(),getppid());
}
}
//保证进程不会死掉
while(1){
sleep(1);
}
}
2.4循环创建n个子进程控制顺序
//实现按顺序退出进程,main进程最后退出
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main(){
int n=5;
int i=0;
pid_t pid=0;
for(i;i<n;i++){
pid=fork();
if(pid==0){
//不同的子进程i的值不同,main进程最后的值为n
printf("I am child%d,pid=%d,ppid=%d",i,getpid(),getppid());
break;//子进程跳出循环,不再创建子进程
}
else if(pid>0){
printf("I am father%d,pid=%d,ppid=%d",i,getpid(),getppid());
}
}
//通过i控制睡眠长度控制按顺序关掉进程
sleep(i);
if(i<n){
printf("I am child%d,will exit,pid=%d\n",i,getpid());
}else{
printf("I am father,will exit,pid=%d\n",getpid());
}
return 0;
}
3.父子进程
3.1父子进程可以共享的内容
看下面的代码
int main(int argc,char*argv){
printf("begin......");
fork();
printf("end........\n");
return 0;
}
begin…没有\n因此存放在缓冲区中。fork()之前的内容属于是共享区,因此end…\n的换行刷新缓冲区,所以打印出两行begin…end…
进程共享
父子进程之间在fork()分叉后,相同处和不同处之处。
刚fork之后:
父子相同处:全局变量,.data区域,.text区域,栈,堆,环境变量(0-3G的用户空间),用户ID,宿主目录,进程工作目录,信号处理方式…
父子不同处:1.进程ID,2.fork返回值,3.父进程id,4.进程运行时间,5.闹钟(定时器),6.未决信号集
子进程与父进程0-3G的用户空间内容相同,并复制了父进程PCB,但是pid不同。
但是子进程并不是复制父进程0-3G的用户空间,父子进程之间满足读时共享写时复制的原则,从而减少空间的花销。
int var=100;
int main()
{
pid_t pid=fork();
if(pid==0){
printf("var=%d,child,pid=%d\n",var,getpid());
var=1001;
printf("var=%d,child,pid=%d\n",var,getpid());
sleep(2);
printf("var=%d,child,pid=%d\n",var,getpid());
}else{
sleep(1);
printf("var=%d,parent,pid=%d\n",var,getpid());
var=1002;
sleep(5)
printf("var=%d,parent,pid=%d\n",var,getpid());
}
return 0;
}
可以看出,父子进程分别修改全局变量的值互不影响,说明满足读时共享写时复制。
3.2exec进程执行族函数
#include <unistd.h>
extern char **environ;
//程序中间嵌入执行其他程序
int execl(const char *path, const char *arg, ...);
//执行程序的时候,使用PATH环境变量,执行的程序可以不用加路径
int execlp(const char *file, const char *arg, ...);
/*
返回值:只有在发生err时才会返回值
参数解释:
path/file:文件名或者路径
arg:可变参数列表
参数列表最后需要NULL结尾,作为哨兵位
执行原理:将当前进程的.text,.data替换为所要加载的程序的.text,.data,然后让进程从新的.text.的第一条指令开始执行。
*/
实例
int main(){
execlp("ls","ls","-l",NULL);
execl("/bin/ls","ls","-l",NULL)
/*
参数解释:
第一个ls为文件名
第二个ls是可变参数列表的第一个参数,与前面的文件名保持一致用于占位。参数列表的最后一位要用NULL
*/
perror("exec err");
return 0;
}
执行结果
3.3孤儿和僵尸进程
孤儿进程:父亲进程结束的子进程,子进程的父进程为init进程
僵尸进程:子进程结束,父进程没有回收子进程的资源(进程管理块PCB);
#include<unistd.h>
//孤儿进程
int main(){
//返回子进程的ID
pid_t pid=fork();
if(pid==0){
printf("I am child,pid=%d\n",getpid());
while(1){
sleep(1);
}
}else{
printf("I am parent,pid=%d\n",getpid());
printf("parent end.....\n")
}
return 0;
}
//僵尸进程,没有回收子进程的资源
int main(){
//返回子进程的ID
pid_t pid=fork();
if(pid==0){
printf("I am child,pid=%d\n",getpid());
}
else{
while(1){
printf("I am father,pid=%d",getpid());
sleep(1);
}
}
return 0;
}
僵尸进程不能使用kill命令清除,因为kill命令只是用来终止进程,而僵尸进程已经终止,只是没有回收资源。
3.4wait回收子进程函数
#include <sys/types.h>
#include <sys/wait.h>
/*
函数作用:
1.阻塞等待进程死亡
2.回收子进程资源
3.调查子进程死亡原因
*/
pid_t wait(int *status);
/*
参数说明:
status是一个传出参数,可以得到进程死亡的原因
返回值:
成功:返回回收子进程的的ID
失败:返回-1
*/
int main(){
pid_t pid=fork();
if(pid==0){
printf("I am child,will die,pid=%d\n",getpid());
sleep(3);
}else if(pid>0){
printf("I am parent,waiting for child die,pid=%d\n",getpid());
//阻塞等待子进程死亡
pid_t wpid=wait(NULL);
printf("pid=%d die",wpid);
}
return 0;
}
输出
I am parent,waiting for child die,pid=23746
I am child,will die,pid=23747
#父进程发生了阻塞,等待子进程死亡再进行后面的程序
pid=23747 die
3.5获得进程死亡原因
/*
进程死亡的原因:
WIFEXITED(status)
如果WIFEXITED为真,即为正常死亡退出。使用WEXITSTATUS(status)得到退出状态。
WIFSIGNALED(status)
如果WIFSIGNALED为真,即为非正常死亡(被信号杀死),使用WTERMSIG(status)得到信号
*/
int main(){
pid_t pid=fork();
if(pid==0){
printf("I am child,will die,pid=%d\n",getpid());
sleep(3);
return 101;
}
else if(pid>0){
printf("I am parent,waiting for child die,pid=%d\n",getpid());
//阻塞等待子进程死亡
int status;
pid_t wpid=wait(&status);
if(WIFEXITED(status)){
printf("child exit with %d\n",WIFEXITED(status));
}
if(WIFSIGNALED(status)){
printf("child killed by %d\n",WTERMSIG(status));
}
printf("pid=%d die",wpid);
}
return 0;
}
3.6waitpid回收子进程函数
pid_t waitpid(pid_t pid, int *status, int options);
/*
参数options:
WNOHANG:如果子进程还活着就立即返回(不是阻塞等待)
WUNTRACED:暂停状态
0和wait相同,会发生阻塞等待
参数pid:
< -1 如果一个进程组ID为pid,传入-pid可以回收该进程组
-1 回收所有任意的进程
0 回收和调用进程组ID相同的组内子进程
> 0 回收ID为pid的进程
返回值:
如果设置了WNOHANG,那么如果没有子进程退出,返回0
如果有,返回子进程pid
如果失败返回-1
其他:失败返回-1
成功:返回子进程pid
*/
int main(){
pid_t pid=fork();
if(pid==0){
printf("I am child ,pid=%d\n",getpid());
sleep(2);
}
else if(pid>0){
printf("I am parent,pid=%d\n",getpid());
//回收任意的子进程,如果进程没有死亡,就返回0;如果有,返回进程ID
int ret=0;
while(ret==0)
{
ret=waitpid(-1,NULL,WNOHANG);
printf("ret=%d\n",ret);
}
if(ret<0){
perror("wait err");
}
printf("child die,child=%d",pid);
}
return 0;
}
3.6.1回收多个子进程
wait函数回收子进程
//用wait回收子进程
int main(){
int n=5;
int i=0;
pid_t pid;
for(;i<n;i++){
pid=fork();
//避免子进程再生成孙子进程
if(pid==0){
break;
}
else if(pid>0){
printf("I am child,pid=%d\n",getpid());
}
}
//按顺序展开进程
sleep(i);
//如果是main进程,就回收子进程
if(i==5){
//wait回收n次子进程
for(i=0;i<n;i++){
pid_t wpid=wit(NULL);
printf("child=%d,回收\n",wpid);
}
}
return 0;
}
waitpid回收子进程
int main(){
int n=5;
int i=0;
pid_t pid;
for(;i<n;i++){
pid=fork();
//避免子进程再生成孙子进程
if(pid==0){
break;
}
}
sleep(i);
//如果是main进程,就回收子进程
if(i==5){
int ret=0;
while(1){
ret=waitpid(-1,NULL,WNOHANG);
if(ret>0){
printf("child=%d回收\n",ret);
}
else if(ret==-1)
{
break;
}
}
}
if(i<n){
printf("I am child,pid=%d\n",getpid());
}
return 0;
}