IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> 系统运维 -> xv6学习笔记——Lab: Xv6 and Unix utilities -> 正文阅读

[系统运维]xv6学习笔记——Lab: Xv6 and Unix utilities


前言

最近在学习MIT经典的操作系统课程——xv6操作系统,之前在本科生期间同样实现过一个简单的操作系统内核,所以代码阅读起来不是特别困难。在这里简单记录一下写实验期间自己的学习笔记,自己实现的代码不一定准确,也希望大佬们多多指正。

一、进程与内存

一个xv6进程在内存中包含两部分。第一部分是用户态的内存,包含有进程的指令、数据以及堆栈;第二部分是进程在操作系统内核中保存的进程表项(在内核态才能够访问),操作系统内核为每一个进程都分配了一个PID进行标识。由于操作系统的第一个实验都是在调用xv6已经写好的几个系统调用函数,在这里结合UNIX常见的系统调用函数,来对一些常用的系统的调用进行功能上的分析(暂时不做实现上的分析,这一部分留到后面的实验具体分析)。
在这里插入图片描述
上图是xv6内核中已经实现的系统调用函数,在此做简要的讲解。

1.1 fork()

头文件

#include<unistd.h>/*#包含<unistd.h>*/
#include<sys/types.h>/*#包含<sys/types.h>*/

函数原型

/*
Clone the calling process, creating an exact copy.
Return -1 for errors, 0 to the new process,
and the process ID of the new process to the old process.
*/
pid_t fork(void)

fork系统调用用于创建一个新进程,称为子进程,它与进程(称为系统调用fork的进程)同时运行,此进程称为父进程。创建新的子进程后,两个进程将执行fork()系统调用之后的下一条指令。子进程使用相同的pc(程序计数器),相同的CPU寄存器,在父进程中使用的相同打开文件。
子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。 UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX系统下,无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork函数中,等待返回。因此fork函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这两次的返回值是不一样的。
fork调用的一个奇妙之处就是它仅仅被调用一次,却能够返回两次,它可能有三种不同的返回值:

  • 在父进程中,fork返回新创建子进程的进程ID;
  • 在子进程中,fork返回0;
  • 如果出现错误,fork返回一个负值。

fork的另一个特性是所有由父进程打开的描述符都被复制到子进程中。父、子进程中相同编号的文件描述符在内核中指向同一个file结构体,也就是说,file结构体的引用计数要增加。

代码示例

#include <unistd.h>
#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
    int pid = fork();
    if (pid > 0)
    {
        //父进程会进入该代码块,因为父进程获得的pid是子进程的pid
        printf("parent:child=%d\n",pid);
        wait((int*)0);
        printf("child %d is done!\n",pid);
    }
    else if(pid == 0){
        //子进程会进入该代码段,因为子进程获得的pid是0
        printf("child:exiting!\n");
        exit(0);
    }
    else
    {
        printf("fork error!\n");
    }
    return 0;
    
}

编译运行结果:

parent:child=1049 //1049不一定每次都相同,因为每次操作系统给子进程分配的进程号不一定相同。
child:exiting!
child 1049 is done!

1.2 wait()

头文件

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

函数原型

/*
Wait for a child to die. When one does, put its status in *STAT_LOC
and return its process ID. For errors, return (pid_t) -1.

This function is a cancellation point and therefore not marked with
__THROW.
*/
pid_t wait(int *__stat_loc)

wait()会暂时停止目前进程的执行, 直到有信号来到或子进程结束。 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值.。子进程的结束状态值会由参数__stat_loc返回, 而子进程的进程识别码也会一起返回、如果不在意结束状态值, 则参数 __stat_loc 可以设成NULL。 子进程的结束状态值参考如下:

WNOHANG:如果没有任何已经结束的子进程则马上返回, 不予以等待.
WUNTRACED:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会. 子进程的结束状态返回后存于status, 底下有几个宏可判别结束情况
WIFEXITED(status):如果子进程正常结束则为非0.
WEXITSTATUS(status):取得子进程exit()返回的结束代码, 一般会先用WIFEXITED 来判断是否正常结束才能使用此宏.
WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真
WTERMSIG(status):取得子进程因信号而中止的信号代码, 一般会先用WIFSIGNALED 来判断后才使用此宏.
WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真. 一般只有使用WUNTRACED时才会有此情况.
WSTOPSIG(status):取得引发子进程暂停的信号代码, 一般会先用WIFSTOPPED 来判断后才使用此宏.

具体的代码示例见1.1节,当父进程调用wait()函数时,就会阻塞当前进程,一直等待子进程运行完毕(打印child:exiting!),之后父进程才会继续执行,打印child 1049 is done!。

补充函数waitpid():
头文件

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

函数原型

/*
Wait for a child matching PID to die.
If PID is greater than 0, match any process whose process ID is PID.
If PID is (pid_t) -1, match any process.
If PID is (pid_t) 0, match any process with the
same process group as the current process.
If PID is less than -1, match any process whose
process group is the absolute value of PID.
If the WNOHANG bit is set in OPTIONS, and that child
is not already dead, return (pid_t) 0. If successful,
return PID and store the dead child's status in STAT_LOC.
Return (pid_t) -1 for errors. If the WUNTRACED bit is
set in OPTIONS, return status for stopped children; otherwise don't.

This function is a cancellation point and therefore not marked with
__THROW.
*/
pid_t waitpid(pid_t __pid, int *__stat_loc, int __options)

**waitpid()**会暂时停止目前进程的执行, 直到有信号来到或子进程结束。如果在调用wait()时子进程已经结束,则wait()会立即返回子进程结束状态值.。子进程的结束状态值会由参数__stat_loc返回,而子进程的进程识别码也会一块返回. 如果不在意结束状态值, 则参数status 可以设成NULL. 参数__pid为欲等待的子进程识别码, 其他数值意义如下:

  1. pid<-1 等待进程组识别码为pid 绝对值的任何子进程。
  2. pid=-1 等待任何子进程, 相当于wait()
  3. pid=0 等待进程组识别码与目前进程相同的任何子进程。
  4. pid>0 等待任何子进程识别码为pid 的子进程。

参数options可以为0或上面状态值。

1.3 exit()

头文件

#include <stdlib.h>

函数原型

/*
Call all functions registered with `atexit' and `on_exit',
in the reverse of the order in which they were registered,
perform stdio cleanup, and terminate program execution with STATUS.
*/
void exit(int __status)

exit()通常是用在子程序中用来终结程序用的,使用后程序自动结束,跳回操作系统,并把参数status 返回给父进程,,而进程所有的缓冲区数据会自动写回并关闭未关闭的文件。exit(0) 表示程序正常退出,exit⑴/exit(-1)表示程序异常退出。

1.4 exec()

在xv6内核中单独实现了一个exec()函数用于创建新进程,但是在实际的Linux系统当中。exec是一个函数族,可以根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。在执行完后,原调用进程的内容除了进程号外,其它全部被新程序的内容替换了。另外,这里的可执行文件既可以是二进制文件,也可以是Linux下任何可执行脚本文件。
在这里插入图片描述
exec函数族的使用场景主要分为以下两种情况:

  • 当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任何exec函数族让自己重生;
  • 如果一个进程想执行另外一个程序,那么它就可以调用fork函数新建一个进程,然后调用任何一个exec函数使子进程重生;

主要区别
这6个函数在函数名和使用语法的规则上都有细微的区别,下面就可执行文件查找方式、参数传递方式及环境变量这几个方面进行比较说明。

  • 查找方式:上表中前4个函数的查找方式都是完整的文件目录路径(即绝对路径),而最后两个函数(也就是以p结尾的两个函数)可以只给出文件名,系统就会自动从环境变量“$PATH”所指出的路径中进行查找。
  • 参数传递方式:有两种方式,一种是逐个列举的方式,另一种是将所有参数整体构造成一个指针数组进行传递。(在这里,字母“l”表示逐个列举的方式,字母“v”表示将所有参数整体构造成指针数组进行传递,然后将该数组的首地址当做参数传递给它,数组中的最后一个指针要求时NULL)
  • 环境变量:exec函数族使用了系统默认的环境变量,也可以传入指定的环境变量。这里以“e”结尾的两个函数就可以在envp[]中指定当前进程所使用的环境变量替换掉该进程继承的所有环境变量。

环境变量
在Linux中,Shell进程是所有执行码的父进程。当一个执行码执行时,Shell进程会fork子进程然后调用exec函数去执行执行码。Sehll进程堆栈中存放着该用户下的所有环境变量,使用不带“e”的4个函数使执行码重生时,Shell进程会将所有环境变量复制给生成的新进程;而使用带“e”的两个函数时新进程不继承任何Shell进程的环境变量,而由envp[]数组自行设置环境变量。

事实上,这6个函数中真正的系统调用只有execve,其他5个都是库函数,它们最终都会调用execve这个系统调用,调用关系如下图所示:
在这里插入图片描述
代码示例

char *const ps_argv[] = {"ps", "-o", "pid, ppid, session, tpgid, comm, NULL"};

char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};

execl("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);

execv("/bin/ps", ps_argv);

execle("/bin/ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL, ps_envp);

execve("/bin/ps", ps_argv, ps_envp);

execlp("ps", "ps", "-o", "pid,ppid,pgrp,session,tpgid,comm", NULL);

execvp("ps", ps_argv);
1 #ifdef HAVE_CONFIG_H
  2 #include <config.h>
  3 #endif
  4 
  5 #include <stdio.h>
  6 #include <stdlib.h>
  7 #include <unistd.h>
  8 #include <string.h>
  9 #include <errno.h>
 10 
 11 int main(int argc, char *argv[])
 12 {
 13   //以NULL结尾的字符串数组的指针,适合包含v的exec函数参数
 14   char *arg[] = {"ls", "-a", NULL};
 15   
 16   /**
 17    * 创建子进程并调用函数execl
 18    * execl 中希望接收以逗号分隔的参数列表,并以NULL指针为结束标志
 19    */
 20   if( fork() == 0 )
 21   {
 22     // in clild 
 23     printf( "1------------execl------------\n" );
 24     if( execl( "/bin/ls", "ls","-a", NULL ) == -1 )
 25     {
 26       perror( "execl error " );
 27       exit(1);
 28     }
 29   }
 30   
 31   /**
 32    *创建子进程并调用函数execv
 33    *execv中希望接收一个以NULL结尾的字符串数组的指针
 34    */
 35   if( fork() == 0 )
 36   {
 37     // in child 
 38     printf("2------------execv------------\n");
 39     if( execv( "/bin/ls",arg) < 0)
 40     {
 41       perror("execv error ");
 42       exit(1);
 43     }
 44   }
 45   
 46   /**
 47    *创建子进程并调用 execlp
 48    *execlp中
 49    *l希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
 50    *p是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件
 51    */
 52   if( fork() == 0 )
 53   {
 54     // in clhild 
 55     printf("3------------execlp------------\n");
 56     if( execlp( "ls", "ls", "-a", NULL ) < 0 )
 57     {
 58       perror( "execlp error " );
 59       exit(1);
 60     }
 61   }
 62   
 63   /**
 64    *创建子里程并调用execvp
 65    *v 望接收到一个以NULL结尾的字符串数组的指针
 66    *p 是一个以NULL结尾的字符串数组指针,函数可以DOS的PATH变量查找子程序文件
 67    */
 68   if( fork() == 0 )
 69   {
 70     printf("4------------execvp------------\n");
 71     if( execvp( "ls", arg ) < 0 )
 72     {
 73       perror( "execvp error " );
 74       exit( 1 );
 75     }
 76   }
 77   
 78   /**
 79    *创建子进程并调用execle
 80    *l 希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
 81    *e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
 82    */
 83   if( fork() == 0 )
 84   {
 85     printf("5------------execle------------\n");
 86     if( execle("/bin/ls", "ls", "-a", NULL, NULL) == -1 )
 87     {
 88       perror("execle error ");
 89       exit(1);
 90     }
 91   }
 92   
 93   /**
 94    *创建子进程并调用execve
 95    * v 希望接收到一个以NULL结尾的字符串数组的指针
 96    * e 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
 97    */
 98   if( fork() == 0 )
 99   {
100     printf("6------------execve-----------\n");
101     if( execve( "/bin/ls", arg, NULL ) == 0)
102     {
103       perror("execve error ");
104       exit(1);
105     }
106   }
107   return EXIT_SUCCESS;
108 }

在xv6内核中,只实现了一个简单的exec()系统调用,比较简单,在此不做过多赘述,代码示例如下:

char *argv[3];
argv[0] = "echo";
argv[1] = "hello";
argv[2] = 0;//结束标志
exec("/bin/echo", argv);
printf("exec error\n");

//实际上就是在终端执行了一个echo hello

xv6使用上述的系统调用来模拟用户执行程序。shell进程是在xv6内核启动的时候就已经开始执行的用户进程,在user/sh.c的shell实现main函数(159行)中,有如下的while循环不断通过getcmd()函数获取用户输入的字符串,当用户完成输入时,父进程将通过fork1()函数创建用于执行用户想要执行的程序的子进程。父进程(shell进程)在wait(0)处被阻塞,等待子进程的结束。子进程首先通过parsecmd()函数将用户输入进行分割,传入runcmd()函数。runcmd()函数发现用户命令是创建新进程并执行时,通过exec()函数将子进程的代码、数据以及堆栈进行替换,如果替换成功,将不会返回runcmd()函数。当用户进程执行完毕后,shell进程的wait()函数收到信号并返回,shell进程继续执行while循环,等待输入用户的下一条命令。
代码分析

while(getcmd(buf, sizeof(buf)) >= 0){
    if(buf[0] == 'c' && buf[1] == 'd' && buf[2] == ' '){
      // Chdir must be called by the parent, not the child.
      buf[strlen(buf)-1] = 0;  // chop \n
      if(chdir(buf+3) < 0)
        fprintf(2, "cannot cd %s\n", buf+3);
      continue;
    }
    if(fork1() == 0)
      runcmd(parsecmd(buf));
    wait(0);
  }

void
runcmd(struct cmd *cmd)
{
  int p[2];
  struct backcmd *bcmd;
  struct execcmd *ecmd;
  struct listcmd *lcmd;
  struct pipecmd *pcmd;
  struct redircmd *rcmd;

  if(cmd == 0)
    exit(1);

  switch(cmd->type){
  default:
    panic("runcmd");

  case EXEC:
    ecmd = (struct execcmd*)cmd;
    if(ecmd->argv[0] == 0)
      exit(1);
    exec(ecmd->argv[0], ecmd->argv);
    fprintf(2, "exec %s failed\n", ecmd->argv[0]);
    break;

    ...
}

fork()函数在分配进程内存时与exec()函数有所不同。fork函数分配足够拷贝父进程内存空间的内存,exec()函数则要分配足够的内存空间供新进程使用。

  系统运维 最新文章
配置小型公司网络WLAN基本业务(AC通过三层
如何在交付运维过程中建立风险底线意识,提
快速传输大文件,怎么通过网络传大文件给对
从游戏服务端角度分析移动同步(状态同步)
MySQL使用MyCat实现分库分表
如何用DWDM射频光纤技术实现200公里外的站点
国内顺畅下载k8s.gcr.io的镜像
自动化测试appium
ctfshow ssrf
Linux操作系统学习之实用指令(Centos7/8均
上一篇文章      下一篇文章      查看所有文章
加:2021-07-22 14:38:04  更:2021-07-22 14:39:34 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/25 16:28:04-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码