第一组 进程创建与管理
1 实验目的及实验要求
在 Linux 环境下,采用 C/C++/Java(或其它语言)编程,完成以下实验:
- 参照 Linux 内核源码结构,阅读 Linux 内核源码,分析 Linux 进程的组成,观察 Linxu 进程的 task_struc 等进程管理数据结构;
-
参照相关示例程序,查阅 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 个时间片。
- 掌握 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
得到的结果是:
父进程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()自然退出。
./file kill 15
:主进程向子进程发送kill(pid, 15),子进程收到信号SIGTERM后程序结束(terminate)。与SIGKILL不同的是该信号可以被阻塞和处理。注:SIGTERM == 15,SIGKILL == 9
./file kill 15 catch
:执行函数signal(15, func),主进程向子进程发送kill(pid, 15),子进程收到该信号后信号被捕获,执行自定义函数func()中的内容,子进程不会被系统结束。这是因为SIGTERM比较友好,进程能捕捉这个信,,根据您的需要来关闭程序。在关闭程序之前,您可以结束打开的记录文件和完成正在做的任务。在某些情况下,假如进程正在进行作业而且不能中断,那么进程可以忽略这个SIGTERM信号。注:SIGTERM == 15
./file kill 9 catch
:执行函数signal(9, func),主进程向子进程发送kill(pid, 9),子进程收到信号SIGKILL后系统立刻结束子进程。因为SIGKILL信号不能被signal捕获,这是一个“我不管您在做什么,立刻停止”的信号。因此运行结果与./file kill 9
相同。注:SIGKILL == 9
./file kill 17 catch
:执行函数signal(17, func),主进程向子进程发送kill(pid, 17),子进程收到SIGCHLD信号后信号被捕获,执行自定义函数func()中的内容。SIGCHLD的作用为,当子进程状态改变时,会向父进程发送SIGCHLD信号,告知父进程自己的状态已改变。因此子进程结束后,主进程捕获子进程发送的SIGCHLD信号,执行func()中的内容。fucn()被子进程和主进程各执行了一遍。注:SIGCHLD == 17。
./file kill 17
:主进程向子进程发送kill(pid, 17)。该信号的默认处理动作是忽略,因此什么都不会发生,子进程自然结束。通常,SIGCHLD信号用于主进程回收子进程。注:SIGCHLD == 17
注:使用命令kill -l
查看系统中全部的信号命令
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
:父子进程切换目录互不影响
./file fs fs
:父进程切换目录会影响子进程,子进程切换目录也会影响父进程
./file vfork
:父进程与子进程单独各自每隔一秒输出一个数字
./file vfork vfork
:父进程clone后立即把控制权交由子进程,并等待子进程结束后,再接着执行
./file files
:父子进程的文件描述符互不影响。子进程关闭文件,不影响父进程正常读取文件
./file files files
:父子进程共享文件描述符。子进程关闭文件,则父进程读文件出错
6 收获与总结
这个实验大约花费了我10个小时完成。在实验中我获得了十分新颖的体验:在对kill和signal进行测试时,我发现如果我使用kill发送信号17,则信号17会被捕获两次,这激起了我对软中断信号的兴趣。经过反复的研究,我发现17信号就是SIGCHILD信号,了解了的SIGCHILD回收子程序的用途。在尝试使用kill发送信号15杀死子程序时,我发现子程序并没有被杀死,经过研究发现这是因为信号被signal捕获,这个发现也让我加深了对signal捕获信号的理解,同时我也了解到了系统对信号的默认处理方式,杀死进程或忽略,而系统的处理方式也是可以用signal修改的(信号9除外)。在完成clone的实验时,我加深了对文件描述符、文件系统的理解,理解了为什么说clone既可以产生进程也可以产生线程,这取决于资源共享的程度。实验巩固了课内所学知识,同时,我还学习到了各种之前没有接触到的api,了解了系统调用与系统命令之间的关系,对我以后的linux编程中对进程的操作和管理有很大帮助。
Comments NOTHING