【北邮大三上操作系统】第一组 进程创建与管理

发布于 2023-07-21  576 次阅读


第一组 进程创建与管理

1 实验目的及实验要求

在 Linux 环境下,采用 C/C++/Java(或其它语言)编程,完成以下实验:

  1. 参照 Linux 内核源码结构,阅读 Linux 内核源码,分析 Linux 进程的组成,观察 Linxu 进程的 task_struc 等进程管理数据结构;
  2. 参照相关示例程序,查阅 Linux 系统调用等相关资料,设计父进程和子进程的业务处理逻辑;利用 C++等高级程序设计语言和 fork()、clone、exec()、wait()、exit()、kill()、getpid()等系统调用,创建管理进程,观察父子进程/线程的结构和并发行为,掌握等待、撤销等进程控制方法。

    要求:

  • 至少用到 fork()、clone、exec、wait、exit、kill、getpid 等 6 个系统调用;

  • 所创建的父子进程各自具有不同的业务处理逻辑,父进程创建子进程后,子进程通过exec()调入自身执行代码;

  • 进程可以自己通过 exit()主动结束,也可以被父进程执行 kill()命令来结束;

  • 观察对比通过 fork()和 clone()创建的子任务/进程/线程的差异,分析 clone()系统调用中设置与不设置 CLONE_FS、CLONE_VFORK、CLONE_FILES、CLONE_FS、CLONE_PID等参数对所创建的子进程/线程的影响;

    CLONE_FS: 子进程与父进程共享相同的文件系统,包括 root、当前目录、umask。
    如果设置该参数,调用者父进程对于 chroot(2), chdir(2),umask(2)的调用,同样会影响到创建的子进程,反之亦然;
    如果没有设置 CLONE_FS,子进程将工作在调用者父进程的文件系统信息的拷贝上,父进程对于 chroot(2), chdir(2),umask(2)的调用不会影响子进程,反之亦然。
    
    CLONE_VFORK: 如果设置该参数,调用者父进程被挂起,直至所创建的子进程通过 execve(2) or _exit(2) (as with vfork(2))释放虚拟内存资源后,父进程才重新激活。
    如果没有设置该参数,父进程和子进程并发执行,都是可调度的。
    
    CLONE_FILES: 如果设置该参数,子进程与父进程共享相同的文件描述符(file descriptor)表。
    调用者父进程创建的文件描述符对于被创建的子进程同样有效,反之亦然。例如,父进程或子进程中的一个关闭了文件描述符,另一个进程也会受影响。
    
    CLONE_PID: 如果设置该参数,子进程创建时得到的 PID 与父进程 PID一致,可用于 hacking the system。
  • 在创建的父子进程/线程代码中的不同位置处增加随机延迟,使得进程执行横跨多个时间片,如通过增加数十到数百毫秒的延迟,保证进程执行时间不少于 3 个时间片。

  1. 掌握 ps、top、pstree –h、vmstat、strace、ltrace、sleep x、kill、jobs 等命令的功能和使用方式,利用这些命令观察进程的行为和特征。

2 实验环境

硬件:阿里云轻量型服务器,2核CPU,内存2G

软件:CentOS 8.2,内核版本为4.18,gcc编译器

3 实验内容一:fork&exec&wait

所创建的父子进程各自具有不同的业务处理逻辑,父进程创建子进程后,子进程通过exec()调入自身执行代码

3.1 实验设计原理

从命令行获取参数

从main()函数的argc和argv参数获取命令行传入参数,若argv[1]为"exec",则执行本段代码。

由子进程通过exec启动程序Dummy

主进程通过fork()创建子进程。如果fork()调用成功,它向父进程返回子进程的PID,并向子进程返回0,即fork()被调用了一次,但返回了两次。子进程通过execl(),执行实现编译好的可执行文件Dummy,并用它来取代原调用子进程的数据段、代码段和堆栈段。在执行完之后,原调用进程的内容除了进程号外,其他全部被新的进程替换了。换言之,对系统而言,还是同一个进程,不过该进程已经执行另一个程序了。

Dummy从键盘获取输入数组,并返回给父进程

Dummy将从键盘获取数字,并将低8位通过return传递给父进程。子进程在main函数结束时,会隐式地调用exit()函数。exit()将删除进程使用的内存空间,同时把退出状态等信息返回给父进程。

父进程等待子进程退出后再退出

为了避免父进程退出后子进程没有退出导致的“孤儿进程”出现,需要调用库函数wait(),让父进程等待子进程退出后再退出,回收子进程残留资源。它将主进程阻塞,直到子进程变为僵尸进程。同时可以通过WEXITSTATUS(status)获取子进程的退出状态,在这里也就是子进程main函数的返回值低8位。

3.2 程序代码

file.c

void parent_func_exec();

int main(int argc, char *argv[]){
    if (argc == 2 && !strcmp(argv[1], "exec")) {
        parent_func_exec();
    }
}

void parent_func_exec() {
    pid_t pid;
    printf("Parent process main begin and fork before... \n");
    pid = fork();
    if (pid < 0) {
        perror("Failed to create child");
        exit(1);
    } else if (pid == 0) {
        // Child
        int result;
        char *cmd = "./Dummy";
        printf("Child process's PID is %d. My parent's PID is %d.\n", getpid(),
               getppid());
        printf("Child process is about to execute \"%s\"\n", cmd);
        result = execl(cmd, cmd, NULL);
        if (result == -1) {
            perror("In child process, failed to exec a program");
        }
        printf("Child process exit before... \n");
        exit(0);
    } else {
        // parent
        int status;
        printf("fork after and in else Parent process's PID is %d.\n", getpid());
        printf("Parent process is waiting ... \n");
        wait(&status);
        if (WIFEXITED(status)) {
            printf("In parent process, child exited with exit code:%d\n", WEXITSTATUS(status));
        } else {
            printf("In parent process, child exited error\n");
        }
    }
    exit(0);
}

Dummy.c

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

int main(int argc, char** argv) {
    int result;
    printf("\nYou are now in a running program \"%s\". \n", argv[0]);
    printf("My PID is %d. My parent's PID is %d.\n", getpid(), getppid());
    printf("Please input an integer (0-255), which will be returned to my parentprocess:\n");
    scanf("%d", &result);
    printf("Goodbye.\n\n");
    return (result & 0377);
}

3.3 实验步骤、运行结果及分析

先使用gcc编译器通过以下指令编译程序:

gcc Dummy.c -o Dummy
gcc file.c -o file

然后执行./file exec得到的结果是:

image-20221207221132080

父进程fork子进程,父进程输出pid并wait子进程,子进程输出pid并调用exec执行子程序,子程序从键盘获取输出(52),使用return返回后退出,父进程获取到子进程的返回值52并输出。

4 实验内容二:kill&signal&sleep

进程可以自己通过 exit()主动结束,也可以被父进程执行 kill()命令来结束

4.1 实验设计原理

从命令行获取参数

从main()函数的argc和argv参数获取命令行传入参数,若argv[1]为"kill",则执行本段代码。

若参数个数为2,则不执行kill()。若参数个数大于等于3,用argv[2]获取第三个参数,作为int kill(pid_t pid, int sig);的第二个传入参数sig。

若参数个数为4,且第四个参数argv[3]为"catch",则使用signal()函数获取kill()发出的信号;否则不使用signal()。

signal()获取kill()信号,并用getpid()显示捕获进程

signal(sig, func)可以捕获kill(pid, sig)发出的信号sig,并执行自定义函数func()中的内容。若不执行signal(),则信号sig不会被捕获,交由系统按SIG_DFL默认的处理方式处理。由此可见,可以将signal()函数置于fork()前,这样,主进程和子进程都可以捕获信号。同时在func中调用getpid(),显示捕获到信号的进程的进程号。由运行结果可以看出,由主进程kill()发出的信号,将在子进程中被signal()捕获。

父进程wait子进程退出并获取子进程退出状态

子进程将sleep()5秒后自然退出,若子进程收到信号,则sleep会被打断,被捕获或交由系统按默认处理。若子进程被系统结束,则不会执行sleep之后的语句。父进程使用wait()获取子进程退出状态,用WIFEXITED()和WIFSIGNALED()分别判断子进程是正常退出/被信号终止退出,用WEXITSTATUS()和WSTOPSIG()分别获取子进程退出的退出状态码/终止信号。

4.2 程序代码

void parent_func_kill(int, int);
void func(int sig);

int main(int argc, char *argv[]){
    if (argc >= 2 && !strcmp(argv[1], "kill")) {
        int MY_SIG = SIGCHLD;
        if (argc >= 3) {
            MY_SIG = atoi(argv[2]);
            if (MY_SIG <= 0) {
                printf("Wrong argument:%s", argv[2]);
                exit(1);
            }
            printf("you are running kill -%d\n", MY_SIG);
            if (argc == 4 && !strcmp(argv[3], "catch")) {
                printf("you will catch signal\n");
                signal(MY_SIG, func);
            } else {
                printf("you will leave signal to sistem\n");
            }
            parent_func_kill(1, MY_SIG);
        } else {
            printf("you will not kill child\n");
            parent_func_kill(0, 0);
        }
    }
}

void parent_func_kill(int is_kill, int MY_SIG) {
    pid_t pid;
    printf("Parent process main begin and fork before... \n");
    pid = fork();
    if (pid == -1) {
        perror("Failed to create child");
        exit(1);
    }
    if (pid == 0) {
        // Child
        printf("Child process's PID is %d. My parent's PID is %d.\n", getpid(),
               getppid());
        printf("Child begin to sleep 5s\n");
        sleep(5);
        printf("Chi1d exit before ...\n");
        exit(0);
    } else {
        // Parent
        printf("fork after and in else Parent process's PID is %d.\n", getpid());
        printf("Parent begin to sleep 1s\n");
        sleep(1);
        printf("Parent awake\n");
        if (is_kill) {
            printf("Parent: Signal %d will be send to child!\n", MY_SIG);
            if (kill(pid, MY_SIG) == 0) {
                printf("send seccess\n");
            } else {
                printf("send failed\n");
            }
        }
        int status;
        printf("Parent process is waiting ... \n");
        wait(&status);
        if (WIFEXITED(status)) {
            printf("In parent process, child exited with exit code:%d\n",
                   WEXITSTATUS(status));
        } else if (WIFSIGNALED(status)) {
            printf("In parent process, child killed with signal:%d\n",
                   WTERMSIG(status));
        } else if (WIFSTOPPED(status)) {
            printf("In parent process, child pause with signal:%d\n",
                   WSTOPSIG(status));
        } else {
            printf("In parent process, child exited with unknown error\n");
        }
    }
}

void func(int sig) {
    printf("It is signal %d in pid = %d processing function!\n", sig, getpid());
}

4.3 实验步骤、运行结果及分析

先使用gcc编译器通过以下指令编译程序:

gcc file.c -o file

以下为几种命令行参数运行方式及对应的解释:

./file kill:执行本段代码,但主进程不发送kill信号。子进程运行到exit()自然退出。

image-20221207231719239

./file kill 15:主进程向子进程发送kill(pid, 15),子进程收到信号SIGTERM后程序结束(terminate)。与SIGKILL不同的是该信号可以被阻塞和处理。注:SIGTERM == 15,SIGKILL == 9

image-20221207231850382

./file kill 15 catch:执行函数signal(15, func),主进程向子进程发送kill(pid, 15),子进程收到该信号后信号被捕获,执行自定义函数func()中的内容,子进程不会被系统结束。这是因为SIGTERM比较友好,进程能捕捉这个信,,根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录文件和完成正在做的任务。在某些情况下,假如进程正在进行作业而且不能中断,那么进程可以忽略这个SIGTERM信号。注:SIGTERM == 15

image-20221207231952484

./file kill 9 catch:执行函数signal(9, func),主进程向子进程发送kill(pid, 9),子进程收到信号SIGKILL后系统立刻结束子进程。因为SIGKILL信号不能被signal捕获,这是一个“我不管您在做什么,立刻停止”的信号。因此运行结果与./file kill 9相同。注:SIGKILL == 9

image-20221208000152059
image-20221208000226822

./file kill 17 catch:执行函数signal(17, func),主进程向子进程发送kill(pid, 17),子进程收到SIGCHLD信号后信号被捕获,执行自定义函数func()中的内容。SIGCHLD的作用为,当子进程状态改变时,会向父进程发送SIGCHLD信号,告知父进程自己的状态已改变。因此子进程结束后,主进程捕获子进程发送的SIGCHLD信号,执行func()中的内容。fucn()被子进程和主进程各执行了一遍。注:SIGCHLD == 17。

image-20221208000317286

./file kill 17:主进程向子进程发送kill(pid, 17)。该信号的默认处理动作是忽略,因此什么都不会发生,子进程自然结束。通常,SIGCHLD信号用于主进程回收子进程。注:SIGCHLD == 17

image-20221208000627283

注:使用命令kill -l查看系统中全部的信号命令

image-20221209145738634

5 实验内容三:clone

观察对比通过 fork()和 clone()创建的子任务/进程/线程的差异,分析 clone()系统调用中设置与不设置CLONE_FS、CLONE_VFORK、CLONE_FILES、CLONE_PID等参数对所创建的子进程/线程的影响;

5.1 实验设计原理

从命令行获取参数

从main()函数的argc和argv参数获取命令行传入参数,若argv[1]为"clone",则执行本段代码。

argv[2]与argv[3]代表观察clone的某种参数,例如:./file clone fs,则观察不设置CLONE_FS的情况,./file clone fs fs,则观察设置CLONE_FS的情况。两种情况相互对照,观察设置与不设置该参数的影响。

为每种参数分别设计函数

clone()的特点为,clone()需要通过参数传入一个函数,clone产生的子进程执行该函数。为了观察每种参数设置与不设置的区别,分别为每种函数设计不同的主程序函数与子程序函数。

通过chdir()验证CLONE_FS

CLONE_FS:子进程与父进程共享相同的文件系统,包括 root、当前目录、umask。如果设置该参数,调用者父进程对于 chroot(2), chdir(2),umask(2)的调用,同样会影响到创建的子进程,反之亦然。

主进程clone()子进程后,子进程使用chdir()改变当前目录到./test/下,父进程和子进程通过get_current_dir_name()输出当前目录;之后父进程使用chdir()改变当前目录到../下,父进程和子进程通过get_current_dir_name()输出当前目录。在这个过程中通过间歇sleep()来控制父进程与子进程的代码执行顺序。如果clone()设置了CLONE_FS参数,则父进程chdir()会使得子进程的目录改变,反之亦然。

验证CLONE_VFORK

CLONE_VFORK:如果设置该参数,调用者父进程被挂起,直至所创建的子进程通过 execve(2) or _exit(2) (as with vfork(2))释放虚拟内存资源后,父进程才重新激活。
如果没有设置该参数,父进程和子进程并发执行,都是可调度的。

通过关闭文件描述符验证CLONE_FILES

CLONE_FILES:如果设置该参数,子进程与父进程共享相同的文件描述符(file descriptor)表。调用者父进程创建的文件描述符对于被创建的子进程同样有效,反之亦然。例如,父进程或子进程中的一个关闭了文件描述符,另一个进程也会受影响。

首先我们通过fd = open()获取文件描述符fd,然后使用clone()创建子进程,并设置CLONE_FILES参数。在子进程中使用close(fd)关闭文件描述符,在父进程中用read(fd)读取文件,则读失败。

CLONE_PID无法验证

如果设置了CLONE_PID ,则创建的子进程的进程ID会与调用进程的进程ID相同。这对于黑客入侵系统很有用,但在其他方面没有太大用处。从Linux 2.3.21开始,该标志只能由系统启动进程(PID 0)进行设置,并在Linux 2.5.16中完全丢弃。如果在flags掩码中指定了该标志,则内核会选择忽略该标志,未来将会回收该标志对应的比特位。

5.2 程序代码

#define STACK_SIZE 65536
// Allocate stack for child task
char *stack;
unsigned long flags = 0;
char buf[256];
int status;
int fd;
void parent_func_CLONE_FILES();
static int child_func_CLONE_FILES(void *arg);
void parent_func_CLONE_FS();
static int child_func_CLONE_FS(void *arg);
int value = 0;
void parent_func_CLONE_VFORK();
static int child_func_CLONE_VFORK(void *arg);
void parent_func_CLONE_PID();
static int child_func_CLONE_PID(void *arg);

int main(int argc, char *argv[]) {
    if (argc < 2) {
        perror("Too few arguments\n");
        exit(1);
    }
    if (argc >= 2 && !strcmp(argv[1], "files")) {
        if (argc == 3 && !strcmp(argv[1], "files")) {
            flags |= CLONE_FILES;
        }
        parent_func_CLONE_FILES();
    }
    if (argc >= 2 && !strcmp(argv[1], "fs")) {
        if (argc == 3 && !strcmp(argv[1], "fs")) {
            flags |= CLONE_FS;
        }
        parent_func_CLONE_FS();
    }
    if (argc >= 2 && !strcmp(argv[1], "vfork")) {
        if (argc == 3 && !strcmp(argv[1], "vfork")) {
            flags |= CLONE_VFORK;
        }
        parent_func_CLONE_VFORK();
    }
    if (argc >= 2 && !strcmp(argv[1], "pid")) {
        if (argc == 3 && !strcmp(argv[1], "pid")) {
            flags |= 0x00001000;  // CLONE_PID
        }
        parent_func_CLONE_PID();
    }
    return 0;
}

void parent_func_CLONE_FILES() {
  stack = (char *)malloc(STACK_SIZE);
  fd = open("file.txt", O_RDWR);
  if (fd == -1) {
    perror("Failed to open file\n");
    exit(1);
  }
  if (!stack) {
    perror("Failed to allocate memory\n");
    exit(1);
  }
  printf("Parent process main begin and clone before... \n");
  if (clone(child_func_CLONE_FILES, stack + STACK_SIZE, flags | SIGCHLD,
            NULL) == -1) {
    perror("Failed to create child\n");
    exit(1);
  }
  printf("Clone after and Parent process's PID is %d.\n", getpid());
  printf("Parent process is waiting ... \n");
  if (wait(&status) == -1) {
    perror("Wait error");
    exit(1);
  }
  printf("In parent process, child exited with exit code:%d\n", WEXITSTATUS(status));
  printf("Try to read file\n");
  status = read(fd, buf, 100);
  if (status < 0) {
    perror("Parent read Failed\n");
    exit(1);
  }
  printf("Parent read:%s\n", buf);
  close(fd);
}

static int child_func_CLONE_FILES(void *arg) {
  printf("Child process's PID is %d. My parent's PID is %d.\n", getpid(),
         getppid());
  printf("Child try to close the file...\n");
  if (close(fd) < 0) {
    perror("Child close file failed\n");
    exit(1);
  }
  printf("Child process exit before... \n");
  exit(0);
}

void parent_func_CLONE_FS() {
  stack = (char *)malloc(STACK_SIZE);
  printf("Parent process main begin and clone before... \n");
  if (clone(child_func_CLONE_FS, stack + STACK_SIZE, flags | SIGCHLD,
            NULL) == -1) {
    perror("Failed to create child\n");
    exit(1);
  }
  printf("Clone after and Parent process's PID is %d.\n", getpid());
  printf("Parent dir: %s\n", get_current_dir_name());
  printf("Parent sleep 1s\n");
  sleep(1);
  printf("Parent awake\n");
  printf("Parent chdir to ./test\n");
  if (chdir("./test") != 0) {
    perror("chdir error\n");
  }
  printf("Parent dir: %s\n", get_current_dir_name());
  printf("Parent sleep 2s\n");
  sleep(2);
  printf("Parent awake\n");
  printf("Parent dir: %s\n", get_current_dir_name());
  printf("Parent process is waiting ... \n");
  if (wait(&status) == -1) {
    perror("Wait error");
    exit(1);
  }
  printf("In parent process, child exited with exit code:%d\n", WEXITSTATUS(status));
}

static int child_func_CLONE_FS(void *arg) { 
    printf("Child process's PID is %d. My parent's PID is %d.\n", getpid(),
         getppid());
    printf("Child dir: %s\n", get_current_dir_name());
    printf("Child sleep 2s\n");
    sleep(2);
    printf("Child awake\n");
    printf("Child dir: %s\n", get_current_dir_name());
    printf("Child chdir to ../\n");
    if (chdir("../") != 0) {
      perror("chdir error\n");
    }
    printf("Child dir: %s\n", get_current_dir_name());
    printf("Child process exit before... \n");
    exit(0);
}

void parent_func_CLONE_VFORK() {
  stack = (char *)malloc(STACK_SIZE);
  printf("Parent process main begin and clone before... \n");
  if (clone(child_func_CLONE_VFORK, stack + STACK_SIZE, flags | SIGCHLD, NULL) ==
      -1) {
    perror("Failed to create child\n");
    exit(1);
  }
  printf("Clone after and Parent process's PID is %d.\n", getpid());
  while (1) {
    printf("father, value = %d\r\n", value);
    value++;
    sleep(1);
    if (value == 5) break;
  }
  exit(0);
}

static int child_func_CLONE_VFORK(void* arg) {
  printf("Child process's PID is %d. My parent's PID is %d.\n", getpid(),
         getppid());
  while (1) {
    printf("child,value = %d\r\n", value);
    value++;
    sleep(1);
    if (value == 5) break;
  }
  printf("Child exit 0\n");
  exit(0);
}

void parent_func_CLONE_PID() {
  stack = (char *)malloc(STACK_SIZE);
  printf("Parent process main begin and clone before... \n");
  if (clone(child_func_CLONE_PID, stack + STACK_SIZE, flags | SIGCHLD,
            NULL) == -1) {
    perror("Failed to create child\n");
    exit(1);
  }
  printf("Clone after and Parent process's PID is %d.\n", getpid());
  printf("Parent process is waiting ... \n");
  if (wait(&status) == -1) {
    perror("Wait error");
    exit(1);
  }
  printf("In parent process, child exited with exit code:%d\n", WEXITSTATUS(status));
}

static int child_func_CLONE_PID(void *arg) {
  printf("Child process's PID is %d. My parent's PID is %d.\n", getpid(),
         getppid());
  printf("Child process exit before... \n");
  exit(0);
}

5.3 实验步骤、运行结果及分析

先使用gcc编译器通过以下指令编译程序:

gcc file.c -o file

然后分别执行以下命令:

./file fs:父子进程切换目录互不影响

image-20221208213806924

./file fs fs:父进程切换目录会影响子进程,子进程切换目录也会影响父进程

image-20221208213851381

./file vfork:父进程与子进程单独各自每隔一秒输出一个数字

image-20221208214059744

./file vfork vfork:父进程clone后立即把控制权交由子进程,并等待子进程结束后,再接着执行

image-20221208214237409

./file files:父子进程的文件描述符互不影响。子进程关闭文件,不影响父进程正常读取文件

image-20221208214820899

./file files files:父子进程共享文件描述符。子进程关闭文件,则父进程读文件出错

image-20221208214919722

6 收获与总结

这个实验大约花费了我10个小时完成。在实验中我获得了十分新颖的体验:在对kill和signal进行测试时,我发现如果我使用kill发送信号17,则信号17会被捕获两次,这激起了我对软中断信号的兴趣。经过反复的研究,我发现17信号就是SIGCHILD信号,了解了的SIGCHILD回收子程序的用途。在尝试使用kill发送信号15杀死子程序时,我发现子程序并没有被杀死,经过研究发现这是因为信号被signal捕获,这个发现也让我加深了对signal捕获信号的理解,同时我也了解到了系统对信号的默认处理方式,杀死进程或忽略,而系统的处理方式也是可以用signal修改的(信号9除外)。在完成clone的实验时,我加深了对文件描述符、文件系统的理解,理解了为什么说clone既可以产生进程也可以产生线程,这取决于资源共享的程度。实验巩固了课内所学知识,同时,我还学习到了各种之前没有接触到的api,了解了系统调用与系统命令之间的关系,对我以后的linux编程中对进程的操作和管理有很大帮助。