进程的创建、等待、退出以及程序替换

1.进程创建

1)fork函数

fork函数从已经存在的进程中创建一个新进程,新进程为子进程,而原进程为父进程。
进程调用fork,当控制转移到内核中的fork代码后内核。

 - 分配新的内存块和内核数据结构给子进程
 - 将父进程部分数据结构内容拷贝到子进程
 - 添加子进程到系统进程列表中
 - fork返回,开始调度器调度

fork有两个返回值,一个接受值(一父多子,一子一父)。

 子进程返回0。
 父进程返回子进程的PID。
 失败返回-1。

父子进程代码共享,数据各自开辟空间,私有一份(采用写时拷贝)。
fork之后通常用if进行分流,父子进程运行具有独立性,由调度算法决定。

#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
  int ret =fork();
  if(ret<0){
      perror("fork");
      return 1;
  }
  else if(ret == 0){//child
         printf("I am child : %d,ret : %d\n",getpid(),ret);
         }else{//father
          printf("I am parent : %d,ret : %d\n",getpid(),ret);
          }
          sleep(1);
          return 0;
 }

2)调度算法了解

1.时间片轮转调度算法(RR) : 给每个进程固定的执行时间,根据进程到达的先后顺序让进程在单位时间片内执行,执行完成后便调度下一个进程执行,时间片轮转调度不考虑进程等待时间和执行时间,属于抢占式调度。优点是兼顾长短作业;缺点是平均等待时间较长,上下文切换较费时。适用于分时系统。
先来先服务调度算法(FCFS) :根据进程到达的先后顺序执行进程,不考虑等待时间和执行时间,会产生饥饿现象。属于非抢占式调度,优点是公平,实现简单;缺点是不利于短作业。
优先级调度算法 (HPF):在进程等待队列中选择优先级最高的来执行。
多级反馈队列调度算法 :将时间片轮转与优先级调度相结合,把进程按优先级分成不同的队列,先按优先级调度,优先级相同的,按时间片轮转。优点是兼顾长短作业,有较好的响应时间,可行性强,适用于各种作业环境。
高响应比优先调度算法 :根据“响应比=(进程执行时间+进程等待时间)/ 进程执行时间”这个公式得到的响应比来进行调度。高响应比优先算法在等待时间相同的情况下,作业执行的时间越短,响应比越高,满足段任务优先,同时响应比会随着等待时间增加而变大,优先级会提高,能够避免饥饿现象。优点是兼顾长短作业,缺点是计算响应比开销大,适用于批处理系统。

3)fork的写时拷贝

通常父子代码共享,父子不再写入时,数据也是共享的,当任意一方试图写入,便以写时拷贝方式各自一份副本。
在这里插入图片描述
写时拷贝考虑因素

- 内存资源
 - 性能,更合理的使用空间

4)fork调用失败原因

  • 系统中有太多的进程
  • 实际用户的进程数量超过了限制

5)vfork函数

  • vfork也是用于创建子进程。而vfork之后的父子进程共享地址空间,也就是说他们除了PCB不一样,别的都一样,fork的子进程则具有独立的地址空间。

  • vfork的子进程保证让子进程先运行,在调用exec后父进程才可能被调度。

 #include<stdio.h>
   #include<sys/types.h>
   #include<unistd.h>
   #include<stdlib.h>
   
   int num=30;
   int main()
   {
      pid_t id=vfork();                                                                                                             
      if(id<0){
          perror("use fork");
          exit(1);
      }
      else if(id==0){//child
          sleep(4);
          num=80;
        printf("child:num=%d\n",num);
        exit(0);
    }
    else{//parent
        printf("parent:num=%d\n",num);
    }
   return 0;
 }

运行结果是:先等待4秒,然后先执行子进程,而子进程对数据修改为80,,父进程也是80,子进程直接改变了父进程的变量值,因为子进程在父进程的地址空间运行。

2.进程等待

1)进程等待的必要性

1.子进程退出,父进程如果不管不顾,就可能造成“僵尸状态”,进而造成内存泄漏
2.进程一旦变成僵尸状态,就刀匠不如,kill -9也无能为力
3.父进程派给子进程的任务完成的如何,我们需要知道,如:子进程运行完成,结果对还是不对,或者是否正常退出
4.父进程通过进程等待的方式,回收子进程,获取子进程退出信息。

2)进程等待的方法

1>wait

#include<sys/types.h>
#include<sys/wait.h>

pid_t wait(int status);
返回值:
    成功返回被等待进程pid,失败返回-1.
参数:
   输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

进程调用wait后会阻塞自己,分析是否存在僵尸子进程,找到就销毁并返回,没找到就一直阻塞。wait只要有一个退出,父进程知道子进程id

2>waitpid

#include<sys/types.h>
#include<sys/wait.>

pid_t waitpid(pid_t pid,int  *status,int  options);d

3)返回值

当正常返回的时候waitpid返回收集到的子进程的进程ID;
如果设置了选项WNOHANG,而调用中waitpid发现没有已经退出的子进程可收集,则返回0;
如果调用中出错,则返回-1,这时error会被设置成相应的值以指示错误所在。

4)参数

1>pid

   pid>0,等待其进程ID与pid相等的子进程,只要等待的子进程还没有结束,waitpid就会一直等下去。
   pid=-1,等待任一个子进程,与wait等效。
   pid=0,等待同一个进程组中的任何子进程,如果子进程已经加入到别的进程waitpid便不会再对它进行处理。
   pid<-1,等待一个指定进程组中的任何子进程,这个进程组的id等于pid的绝对值。

2>status

  1.WIFEXITED(status) 判断子进程是否为正常退出的,如果是,它会返回一个非零值
  2.WEXITSTATUS(status) 当WIFEXITED返回非零值时,我们可以用这个宏 来提取子进程的退出码,如果子进程调用exit(25)退出,WEXITSTATUS(status)就会返回25。如果进程不是正常退出的,WIFEXITED返回0,这个值就无意义。

3>options

      WNOHANG(非阻塞 1):若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待,若正常结束,则返回该子进程的ID。
      WUNTRACED(阻塞 0)

这是两个常数,可以使用“|”把他们连起来使用,eg:waitpid(-1,NULL,WNOHANG|WUNTRACED);
如果不使用设为0就好,(-1,NULL,0);

阻塞与非阻塞理解:假如梦先生在等烧水,阻塞式表示他一直在等,子进程没有退出,父进程就卡住;非阻塞式表示,他可以在等的过程中干别的比如玩玩手机,聊聊天什么,子进程没退出,父进程可退出,不会卡住。

5)获取子进程的status

  • wait和waitpid,都有一个status参数,该参数是一个输出型参数,由操作系统填充
  • 如果传递NULL,表示不关心子进程的退出状态信息
  • 否则,操作系统会根据该参数,将子进程的子进程信息反馈给父进程
  • status不能简单的当做整型来看待,只有它的低16位被用作status,可以当做位图来看待,具体细节如下:
    正常终止次2低8位(8-15)是退出码,0~7是0。
    被信号杀死,status的低7位是退出码。

    在这里插入图片描述

6)wait使用

#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>

int main(){                                                                                                                                                             
    pid_t pid=fork();
    if(pid==-1){
        perror("use fork");
        exit(1);
    }   
    else if(pid==0){//child
        sleep(5);
        exit(10);
    }   
    else{//parent
        int st;//status
        int ret=wait(&st);
        if(ret>0&&(st&0X7F)==0)
            printf("child exit code is [%d]\n",(st>>8)&0XFF);
         else
            printf("sig code is[%d]\n",st&0X7F);
    }       
} 


开启两个终端,一个执行后,打开另一个终端,杀死子进程,再执行一次。

7)waitpid的使用

阻塞式

#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>
int main(){
    pid_t id=fork();                                                                                                                                                    
    if(id<0){
        perror("use fork");
        exit(1);
    }   
    else if(id==0){//child
        printf("child is run,pid is [%d]\n",getpid());
        sleep(5);
        exit(37);
    }   
    else{//parent
        int st;//status
        pid_t ret=waitpid(-1,&st,0);//0代表阻塞式
        printf("this is test for waitpid\n");
        if(WIFEXITED(st)&&ret==id)//WIFEXITED判断是否正常返回
            printf("wait child 5s success,child return code is [%d]\n",WEXITSTATUS(st));
            //WEXITSTATUS宏获取退出码
        else{
            printf("wait child failed,return.\n");
            return 1;
        }
    }
    return 0;
}

在这里插入图片描述

非阻塞式

#include<stdio.h>
#include<stdlib.h>
#include<sys/wait.h>
#include<unistd.h>

int main(){
    pid_t id=fork();
    if(id<0){
        perror("use fork");
        exit(1);
    }   
    else if(id==0){//child
        printf("child is run,pid is [%d]\n",getpid());
        sleep(5);
        exit(1);
    }   
    else{//parent
        int st;//status
        pid_t ret=0;
        do{ 
            ret=waitpid(-1,&st,WNOHANG);//WNOHANG是1,代表非阻塞式等待
            if(ret==0){
                printf("child is running\n");
            }   
            sleep(1);
        }while(ret==0);
        if(WIFEXITED(st)&&ret==id)
            printf("wait child 5s success,child return code is [%d]\n",WEXITSTATUS(st));
        else
            printf("wait child failed,return.\n");
            return 1;
    }   
    return 0;
} 

在这里插入图片描述

3.进程的程序替换

1)基本概念

用fork创建子进程后执行的是和父进程相同的程序,当然可以执行不同的分支(如果我们用fork创建一个子进程之后让子进程做和父进程同样的事,那么这个子进程没有任何意义)。
所以在fork之后,我们应该调用exec函数用来替换子进程的程序和数据,让子进程执行和负进程不同的程序。当进程调用exec函数时,该进程的用户空间得到代码和数据完全被新的程序替换。
调用exec并不创建新的进程,所以调用exec前后的进程的id并未改变。
在这里插入图片描述

2)替换函数

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 execvpe(const char *file, char *const argv[],char *const envp[]);

3)函数解释

这些函数如果调用成功则加载新的程序从启动端开始执行,不再返回。
如果出错,返回-1.

4)命名理解

1.l:表示参数采用列表例如:execl("/bin/ls",“ls”,"-a",“NULL”)
2.v:参数用数组
例如:
char* myargv[]={“ls”,"-a,“NULL”};
execv(“bin/ls”,myargv);
3.p:有p自动搜索环境变量PATH
4.e:表示自己维护环境变量
在这里插入图片描述

5)使用

char * const argv[ ] ={ "ps","-ef",NULL);
char * const envp[ ]={"PATH=/bin:/usr/bin","TERM=console",NULL);

execl("/bin/ps","ps","-ef",NULL);


//带P的,可以使用环境变量PATH,无需写全路径
execlp("ps","ps","-ef",NULL");

//带e的自己组装环境变量
execle("ps","ps","-ef",NULL,envp);
execv("/bin/ps",argv);
//带P的,可以使用环境变量PATH,无需写全路径
execvp("ps",argv);
//带e的自己组装环境变量
execvp("/bin/ps",argv,envp);

注:这6个函数只有execve是真正的系统调用,其他5个函数都是在execve上包装的,它们6个函数有以下的关系。

在这里插入图片描述

4.进程退出

1)进程退出场景

1.代码运行完毕,结果正确
2.代码运行完毕,结果不正确
3.代码异常终止

2)进程常见退出方法

a.正常退出

1.man函数返回
2.调用exit,
3._exit

b.异常退出

1.ctrl+c ,信号终止

3)_exit与exit函数

a._exit函数

#include<unistd.h>
void _exit(int status);

参数status定义了进程的终止状态,父进程通过wait获取该值(0正常返回,1异常返回),虽然status是int,但是仅有8低8位可以被父进程所用,所以_exit(-1)时,在终端执行$?发现返回值为255.

b.exit函数

#include<unistd.h>
void _exit(int status);
  • exit与_exit函数的区别:exit刷新缓存区,_exit不刷新。
  • return退出
    return退出是一种常见的进程退出方法,return m等同于exit(m),因为调用main的运行时函数会把main的返回值当做exit的参数。

5.setenv函数

作用:改变或增加环境变量
相关函数getenv,putenv,unsetenv,首先要说明的是,通过此函数并不能添加或修改shell进程的环境变量,或者说通过setenv函数设置的环境变量只在本进程,而且是本次执行中有效。如果在某一次运行程序时执行了setenv函数,进程终止再次运行该程序,上次的设置是无效的,上次设置的环境变量是不能读到的。
参数value则为变量内容,参数overwrite用来决定是否要改变已存在的环境变量。注释stdlib.h在Linux和windows中略不同,比如setenv函数是用在linux中的,在Windows中没有setenv函数而用putenv来代替。
定义函数:int setenv(const char *name,const char *value,int overwrite);
函数说明setenv函数用来改变环境变量或增加环境变量的内容,参数name为环境变量名称字符串,参数value则为变量的内容,参数overwrite用来决定是否要改变已存在的环境变量。如果没有此环境变量,则无论overwrite为何值均添加此环境变量。若此环境变量存在,overwrite不为0时,原内容会被改为参数value所指的变量内容,当overwrite为0时,则参数会被忽略。返回值执行成功则返回0,有错误发生时,返会-1

说明:通过此函数并不能添加或修改 shell 进程的环境变量,或者说通过setenv函数设置的环境变量只在本进程,而且是本次执行中有效。如果在某一次运行程序时执行了setenv函数,进程终止后再次运行该程序,上次的设置是无效的,上次设置的环境变量是不能读到的。
语法setenv [变量名称] [变量值]

6.export函数

1)作用

设置或显示环境变量

2)说明

在shell中执行程序时,shell会提供一组环境变量。export可新增,修改或删除环境变量,供后续执行的程序使用。export的效力仅及于该次登陆操作。

3)语法

export [-fnp] [变量名称] = [变量设置值]

参数说明:

-f  代表[变量名称]中为函数名称。

-n  删除指定的变量。变量实际上并未删除,只是不会输出到后续指令的执行环境中。

-p  列出所有的shell赋予程序的环境变量。

猜你喜欢

转载自blog.csdn.net/weixin_41892460/article/details/84581075