进程控制

  1. 学习进程创建, 等待, 终止. 使用代码实现.
  2. 编写自主shell.
  3. 封装fork/wait等操作, 编写函数 process_create(pid_t* pid, void* func, void* arg), func回调函数就是子进程执行的入口函数, arg是传递给func回调函数的参数.
  4. 调研popen/system, 理解这两个函数和fork的区别.
    写一篇博客, 总结上述内容. 作业以链接形式提交(代码也是在博客中体现).

进程控制分为:
1.进程创建
2.进程等待
3.程序替换
4.进程终止
下面先来介绍进程的创建:
1.创建进程<1>.申请空白PCB,为新进程申请获得唯一的数字标识符,并从PCB集合中索取一个空白的PCB。
<2>. 为新进程分配其运行所需的资源,包括各种物理和逻辑资源,如内存、文件、I/O设备和CPU时间等。新进程对这些资源的需求一般也要提前告知系统或其父进程。
<3>.初始化进程控制块(PCB)。包括:a.初始化标识信息,将系统分配的标识符和父进程标识符填入新的PCB中。b.初始化处理机状态信息,是程序计数器指向下一个程序地址。c.初始化处理机控制信息,将进程的状态设置为就绪状态,对于优先级,通常将它设置为最低优先级。
<4>.如果进程就绪队列能够接纳新的进程,便将新的进程插入就绪队列。

进程创建的函数:
fork函数:它从已存在的进程中创建一个新的进程。新进程称为子进程,原进程为父进程。
返回值:子进程中返回0,父进程中返回子进程id,出错返回-1。
父进程与子进程代码共享,数据各自开辟空间,采用写时拷贝。

进程调用fork,当控制转移到内核中的fork代码后,
内核所做的事情为:
1>.给子进程分配新的内存块和内核数据结构
2>.将父进程的部分数据结构拷贝给子进程。
3>.将子进程添加到系统进程中。
4>.fork返回,开始调度器调度。
关于fork函数的代码:
这里写图片描述
这里写图片描述
fo所以fork之前父进程独立执行,fork之后,父子两个执行流分别执行,但是,fork之后,实现执行完全由调度器决定
关于fork函数父子进程进程写时拷贝。
因为其父进程与子进程代码共享,数据各自开辟空间,采用写时拷贝。
<1>下面来介绍写时拷贝:
这里写图片描述
从上图中可看到,其数据段各自开辟空间。
<2>.fork 调用失败的原因:
.系统中有太多进程。
.实际用户的进程数超出了限制。
另外来讲讲另一个进程创建函数:
vfork函数 :用来创建子进程。而子进程和父进程共享地址空间。
保证子进程先运行,在它调用exec或(exit)之后父进程才可能被调度运行。

这里写图片描述
这里写图片描述
子进程直接改变了父进程的变量值,因为子进程在父进程的地址空间中进行。
若使用vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数退出。

下面说说fork函数与vfork函数的区别:

fork函数vfork函数
fork函数子进程具有独立的地址空间。vfork函数子进程与父进程共享一块地址空间,子进程直接改变父进程的变量值。
fork函数子进程与父进程运行先后顺序不变。.vfork函数保证子进程先运行,等到子进程执行exec函数(或exit)之后父进程才可能被调度执行。vfork()创建子进程后,父进程会被阻塞,直至子进程调用exec或者_exit函数。

2.进程等待
进程等待的意义:<1>.回收子进程的资源。<2>.获得子进程的退出信息。
进程等待的方法:
wait方法:pid_t wait(int *status)
返回值:成功返回被等待进程pid,失败返回-1。
参数:输出型参数,通过操作系统赋予的,获取子进程的退出状态,不关心则可设置为NULL。

功能为:

1.阻塞当前进程,直到有子进程退出才返回。
2.回收子进程的残留资源。
3.获取子进程的退出状态。

waitpid方法:pid_t waitpid(pid_t pid,int *status,int options);

返回值:参数
当正常返回时pid返回收集到的子进程的进程IDpid:pid=-1,等待任一个子进程。与wait等效 pid>0,等待其进程ID与pid相等的子进程。
如果设置了选项WNOHANG,而调用中waitpid发现没有已推出的子进程可以收集,则返回0。status:WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status):若WIFEXITED非0,提取子进程退出码。(查看进程的退出码)。
如果调用中出错,则返回-1,这时error会被设置为相应的值以指示错误所在。options:WNOHANG:若pid指定的子进程没有结束,则waitpid()函数返回0,不予以等待。若正常结束,则返回该进程的ID。

总结:
1.如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退出信息。
2.如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。
3.如果不存在子进程,则立即出错返回。
那么现在来看看获取子进程的status:
上面对status参数已经做过了介绍:
下面将其当做一个位图对待。
这里写图片描述
下面来看段代码:
这里写图片描述
这里写图片描述
下面来依次举例阻塞式等待与非阻塞式等待:
1.阻塞式等待:
这里写图片描述
waitpid(-1,&status,0)//相当于wait.为阻塞式等待。
status:WIFEXITED(status):若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出) WEXITSTATUS(status):若WIFEXITED非0,提取子进程退出码。(查看进程的退出码)。
运行结果为:
这里写图片描述
2.非阻塞式等待:

  1 #include<stdio.h>
  2 #include<unistd.h>
  3 #include<stdlib.h>
  4 #include<sys/wait.h>
  5 
  6 int main()
  7 {
  8         pid_t pid=fork();
  9         if(pid<0){
 10         perror("fork()");
 11         exit(1);}
 12         else if(pid==0){
 13         printf("child run,pid id %d\n",getpid());
 14         sleep(5);
 15         exit(1);
 16         }
 17         else{
 18                 int status=0;
 19                 int ret=0;
 20                 do{
 21                 ret=waitpid(-1,&status,WNOHANG);//not block waiting
 22                 if(ret==0){
 23                         printf("child id running\n");
 24                 }sleep(1);
 25         }while(ret==0);
 26         if(WIFEXITED(status)!=0)
 27         {
 28                 printf("wait child 5s sucess,child return code is %d\n",WEXITSTATUS(status));
 29         }else{
 30                 printf("wait child failed,return \n");
 31                 return 1;
 32                 }
 33         }
 34         return 0;
 35 }

结果显示为:
这里写图片描述
3.进程程序替换:以函数形式将程序加载至内存中。

exec族的任一函数都不创建一个新的进程,而是在调用进程里面去执行新的程序。所以进程id不变,还是调用exec函数前的进程id,但是用户空间的代码和数据都更新了,变为新程序的代码和数据了。

extern char **environ;  //全局环境变量,导入到本文件即可直接使用
  1. int execl(const char *path, const char *arg, …);

    功能:通过路径+文件名来加载一个进程;path文件路径;arg文件名称;…可变参数,至少一个NULL

    附:l即list

    返回值:成功的情况下是没有返回的,失败时返回-1 。

    举例说明:

    execl("/bin/ls", "ls", "-a", "-l", NULL);   //path绝对路径,如/bin/ls;文件名称ls;后面三个可变参数,最后必须以NULL结束
    
  2. int execlp(const char *file, const char *arg, …);

    功能:借助PATH环境变量加载一个进程,file要加载的程序的名称

    附:l即list;p即path

    该函数需要配合PATH环境变量来使用,当PATH中所有目录搜索后没有参数file,则出错返回。

    该函数通常用来调用系统程序。如:ls、cp、cat等命令。

    返回值:成功的情况下是没有返回的,失败时返回-1 。

    举例说明:

    execlp("ls", "ls", "-a", "-l", NULL);   //第一个ls是指查看PATH环境变量里的ls;第二个ls是名称文件;后面是可变参数,NULL结束
    
  3. int execle(const char path, const char *arg, …, char const envp[]);

    功能:加载指定路径的程序,并为新程序复制最后一个环境变量

    附:l即list;e即environment

    举例说明:

    char* envp[] = {NULL};
    
    execlp("ls", "ls", "-a", "-l", NULL, envp);
    
  4. int execv(const char *path, char *const argv[]);

    功能:加载指定路径的程序

    附:v即vector,命令行参数列表

    举例说明:

    char* argv[] = {"ls", "-a", "-l", NULL}; 
    
    execl("/bin/ls",argv);
    
  5. int execvp(const char *file, char *const argv[]);

    功能:加载path环境变量里的名称为file的程序

    附:v即命令行参数列表,p即path

    int main(int argc, char *argv[]) {

    pid_t pid = fork();
    
    if (pid == 0) { //子进程里加载ls程序
    
        char* argvv[] = {"ls", "-a", "-l", NULL};
    
        execvp("ls", argvv);
    
        perror("execlp");   exit(1);    //只有execl函数执行失败的情况下才有机会执行这两句代码,执行的成功话就有去无回了。
    
    } else if (pid > 0) {
    
        sleep(1);   printf("parent\n");
    
    }
    
    return 0;
    

    }

  6. int execve(const char *filename, char *const argv[], char *const envp[]);

    功能:加载指定的程序;filename必须是一个可执行程序或者一个以#! interpreter [optional-arg] 开始的脚本。

    上面的五个exec函数是库函数,这个是系统函数;上面的五个exec函数最终都是调用这个函数实现的。 
    

    总结:exec族函数的规律

    exec函数一旦调用成功就有去无回了,去执行新的程序去了。只有失败时才有返回,返回值为-1。所以我们直接在exec函数调用后直接调用perror()和exit(),不需要if判断,因为失败的情况才会执行。

    函数名的意义的理解:

    l (list)    命令行参数列表
    
    p (path) 环境变量,环境变量搜素文件名称file
    
    v (vector) 命令行参数数组
    
    e (environment) 环境变量数组,给新加载程序设置指定的环境变量
    

4.进程终止
进程退出的场景:<1>.代码运行完毕,结果正确。<2>.代码运行完毕,结果不正确。<3>.代码异常终止。

正常终止:
1.从main返回。
2.调用exit。
3._exit。
异常退出:信号终止

exit函数:
void exit(int status);
exit最后也会调用_exit,但在调用_exit之前,做了:
1.执行用户通过atexit或on_exit定义的清理函数。
2.关闭所有打开的流,所有的缓存数据均被写入。
3.调用_exit.

下面来说说return 函数与exit函数。
执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返回值当做为exit的参数。
那再看来一段代码:
在子进程中使用return就会挂掉:

//bad_vfork.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int glob = 6;
int
main(void)
{
    int var;
    pid_t pid;
    var = 88;
    printf("before vfork\n");
    if ((pid = vfork()) < 0) {
    printf("vfork error");
    exit(-1);
    } else if (pid == 0) 
    {
    /* 子进程 */
    glob++;
    var++;
    return 0;
    }
    printf("pid=%d, glob=%d, var=%d\n", getpid(), glob, var);
    return 0;
}

从上面我们知道,结束子进程的调用是exit()而不是return,如果你在vfork中return了,那么,这就意味main()函数return了,
注意因为函数栈父子进程共享,所以整个程序的栈就跪了。

如果你在子进程中return,那么基本是下面的过程:

1)子进程的main() 函数 return了,于是程序的函数栈发生了变化。
2)而main()函数return后,通常会调用 exit()或相似的函数(如:_exit(),exitgroup())
3)这时,父进程收到子进程exit(),开始从vfork返回,但是尼玛,老子的栈都被你子进程给return干废掉了,你让我怎么执行?(注:栈会返回一个诡异一个栈地址,对于某些内核版本的实现,直接报“栈错误”就给跪了,然而,对于某些内核版本的实现,于是有可能会再次调用main(),于是进入了一个无限循环的结果,直到vfork 调用返回 error
好了,现在再回到 return 和 exit,return会释放局部变量,并弹栈,回到上级函数执行。

exit直接退掉。如果你用c++ 你就知道,return会调用局部对象的析构函数,exit不会。(注:exit不是系统调用,是glibc对系统调用_exit()或_exitgroup()的封装)

可见,子进程调用exit() 没有修改函数栈,所以,父进程得以顺利执行。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值