实验报告
实验内容
编译运行课件 Lecture 06 例程代码:Algorithms 6-1 ~ 6-6。并对实验内容的原理性和实现细节进行解释,包括每个系统调用的作用过程和结果。
实验环境
Ubuntu 20.04.2.0(64位)
实验过程
一. alg.6-1-fork-demo.c
(一)
命令行:
$ gcc -o alg.6-1-fork-demo alg.6-1-fork-demo.c
$ ./alg.6-1-fork-demo
输出结果:
Parent pro pid = 3805, child pid = 3806, count = 1 (addr = 0x7fffbe37c180)
Child pro pid = 3806, count = 2 (addr = 0x7fffbe37c180)
Testing point by 3806
Testing point by 3805
(二)对代码中不熟悉的内容进行解析:
- pid_t (参考资料)
使用 pid_t
需要加头文件 #include <sys/types.h>
pid_t
是一个typedef定义类型。
用它来表示进程id类型。
sys/types.h:
typedef short pid_t; /* used for process ids */
pid_t
就是一个short类型变量,实际表示的是内核中的进程表的索引
使用pid_t
而不使用int
只是为了可移植性好一些。
因为在不同的平台上有可能typedef int pid_t
也有可能typedef long pid_t
- fork() (参考资料)
①fork()是创建进程函数。
②c程序一开始,就会产生一个进程,当这个进程执行到fork()的时候,会创建一个子进程。
③此时父进程和子进程是共存的,它们俩会一起向下执行c程序的代码。
④子进程创建成功后,fork是返回两个值,一个代表父进程,一个代表子进程:代表父进程的值是一串数字,这串数字是子进程的ID(地址);一个代表子进程,值为0。
- perror() (参考资料)
使用 perror()
需要加头文件 #include <stdio.h>
函数定义:
void perror(const char *s);
例:perror("fork()");
函数说明:
字符串s: 错误原因
例:
perror("testfile");
出错时输出:
testfile: No such file or directory
- getpid()/getppid() (参考资料)
getpid()返回当前进程标识,getppid()返回父进程标识。
- sleep() (参考资料)
使用 sleep()
需要加头文件 #include <unistd.h>
函数功能:将进程挂起,时间为秒
函数原型:unsigned int sleep(unsigned int seconds);
参数:挂起的时间数 ,单位为秒
返回值:若进程/线程挂起到参数所指定的时间则返回0,若有信号中断则返回剩余秒数。
- wait() (参考资料)
使用 wait()
需要加头文件 #include <sys/types.h>
和 #include <sys/wait.h>
函数原型:pid_t wait(int *status);
参数:wait()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用wait()时子进程已经结束, 则wait()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status返回, 而子进程的进程识别码也会一块返回. 如果不在意结束状态值, 则参数status可以设成NULL.
返回值:如果执行成功则返回子进程识别码(PID), 如果有错误发生则返回-1. 失败原因存于errno中.
- EXIT_SUCCESS (参考资料)
EXIT_FAILURE和EXIT_SUCCESS是C语言头文件库中定义的一个符号常量,在vc++6.0下头文件stdlib.h中定义如下:
#define EXIT_FAILURE 1
#define EXIT_SUCCESS 0
EXIT_FAILURE可以作为exit()的参数来使用,表示没有成功的执行一个程序。
EXIT_SUCCESS可以作为exit()的参数来使用,表示成功地执行一个程序。
(三)运行过程:
程序运行到 childpid = fork();
,该进程会创建一个子进程。
若创建失败,输出错误原因并返回失败标志。
若创建成功,系统会为子进程分配资源并将父进程的相关内容全部复制到子进程中。然后fork()函数会给父进程和子进程各自返回一个返回值,使得父进程 childpid = 子进程的ID(地址)
,子进程 childpid = 0
。
此时父进程和子进程是共存的,且虚拟地址相同,它们俩会从该位置一起向下执行c程序的代码。
子进程进入 if (childpid == 0)
分支,父进程进入相应的 else
分支。
父进程先执行分支里的 printf()
,然后 sleep(5)
挂起5秒,并且 wait(0)
要等待子进程结束后才能继续执行下面的语句。
子进程没有任何阻碍,一直往下执行,直到返回结束。
sleep(5)
之前的父子进程执行次序不确定,靠操作系统来进行调度。
因此,最后 Testing point
是子进程先输出并结束,等待5秒后父进程才能输出。
二. alg.6-2-vfork-demo.c
(一)
命令行:
$ gcc -o alg.6-2-vfork-demo alg.6-2-vfork-demo.c
$ ./alg.6-2-vfork-demo
输出结果:
Child pro pid = 4759, count = 2 (addr = 0x7ffcb570f5e0)
Child taking a nap ...
Child waking up!
Parent pro pid = 4758, child pid = 4759, count = 2 (addr = 0x7ffcb570f5e0)
Testing point by 4758
(二)对代码中不熟悉的内容进行解析:
- vfork() (参考资料)
①vfork()产生的子进程与父进程共享数据段上的数据
②由vfork产生的子进程一定会先于父进程运行,期间会将父进程挂起,直到子进程运行结束或者调用了exec()系列函数才会重新调度父进程。
③fork()和vfork()的区别联系:
1.
fork():子进程拷贝父进程的前3G的地址空间的内容.
vfork():子进程与父进程共享数据段,读写都共享.
2.
fork():父子进程的执行次序不确定,最终由调度进程决定.
vfork():保证子进程先运行,父进程会被挂起,直到子进程调用exit()或者exec()系列函数.
如果在调用这两个函数之前子进程有依赖于父进程的进一步动作,则会导致死锁,因为父进程已经被挂起,父进程的运行需要子进程的退出,而子进程的执行又依赖父进程.
3.
vfork()用于创建一个新进程,而该新进程的目的是exec一个新进程,通常vfork()和exec搭配使用.
vfork()和fork()一样都创建一个子进程,但是它并不将父进程的地址空间完全复制到子进程中,不会复制页表.
因为子进程会立即调用exec,于是也就不会存放该地址空间.
不过在子进程中调用exec或exit之前,他在父进程的空间中运行。
vfork()这个系统调用是用来启动一个新的应用程序.
其次,子进程在vfork()返回后直接运行在父进程的栈空间,并使用父进程的内存和数据,这是很危险的操作.
子进程还必须避免改变全局数据结构或全局变量中的任何信息,因为这些改变都有可能使父进程不能继续.
通常,如果应用程序不是在fork()之后立即调用exec(),就有必要在fork()被替换成vfork()之前做仔细的检查。
- _exit(0) (参考资料)
_exit(0): 不能输出结果,未清除I/O缓存,不打印.
exit(0): 正常运行并退出当前进程.
exit(1): 非正常退出当前进程.
_exit(): 直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit(): 在_exit()基础上作了一些包装,在执行退出之前加了若干道工序。
exit()函数与_exit()函数最大的区别就在于 exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件。
(三)运行过程:
程序运行到 childpid = vfork();
,该进程会创建一个子进程。
若创建失败,输出错误原因并返回失败标志。
若创建成功,子进程与父进程共享地址空间。vfork()函数会给父进程和子进程各自返回一个返回值,使得父进程 childpid = 子进程的ID(地址)
,子进程 childpid = 0
。
此时子进程先于父进程运行,期间会将父进程挂起,直到子进程运行结束才会重新调度父进程。
子进程进入 if (childpid == 0)
分支,逐步运行到返回结束。
子进程结束后,父进程进入相应的 else
分支,逐步运行到返回结束。其中因为子进程结束后父进程才执行 wait(0)
,所以wait直接返回子进程结束状态值,即0。
因此,子进程的输出全部先于父进程的输出。
三. alg.6-3-fork-demo-nowait.c
(一)
命令行:
$ gcc -o alg.6-3-fork-demo-nowait alg.6-3-fork-demo-nowait.c
$ ./alg.6-3-fork-demo-nowait
输出结果:
Parent pro pid = 4812, child pid = 4813, count = 1 (addr = 0x7ffe11d502a0)
Testing point by 4812
child pro pid = 4813, count = 2 (addr = 0x7ffe11d502a0)
child sleeping ...
命令行:
$ ps -l
输出结果:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 2081 2073 0 80 0 - 4877 do_wai pts/0 00:00:00 bash
1 S 1000 4813 1382 0 80 0 - 624 hrtime pts/0 00:00:00 alg.6-3-fo
4 R 1000 4814 2081 0 80 0 - 5013 - pts/0 00:00:00 ps
命令行:
$
输出结果:
child waking up!
Testing point by 4813
(二)运行过程:
程序运行到 childpid = fork();
,该进程会创建一个子进程。
若创建失败,输出错误原因并返回失败标志。
若创建成功,系统会为子进程分配资源并将父进程的相关内容全部复制到子进程中。然后fork()函数会给父进程和子进程各自返回一个返回值,使得父进程 childpid = 子进程的ID(地址)
,子进程 childpid = 0
。
此时父进程和子进程是共存的,且虚拟地址相同,它们俩会从该位置一起向下执行c程序的代码。
子进程进入 if (childpid == 0)
分支,父进程进入相应的 else
分支。
子进程顺序执行到 sleep(10)
挂起10秒,10秒后继续执行下面的语句。
父进程没有任何阻碍,一直往下执行,直到返回结束。
在这挂起的10秒钟,通过 ps -l
可以看到子进程正在sleep,且ppid不是父进程的id,说明父进程已结束,子进程交给init process。
sleep(10)
之前的父子进程执行次序不确定,靠操作系统来进行调度。
四. alg.6-4-fork-demo-wait.c
(一)
命令行:
$ gcc -o alg.6-4-fork-demo-wait alg.6-4-fork-demo-wait.c
$ ./alg.6-4-fork-demo-wait
输出结果:
child pro pid = 4897, count = 2 (addr = 0x7ffe4f64070c)
child sleeping ...
child waking up!
Testing point by 4897
Parent pro pid = 4896, terminated pid = 4897, count = 1 (addr = 0x7ffe4f64070c)
Testing point by 4896
命令行:
$ ps
输出结果:
PID TTY TIME CMD
2081 pts/0 00:00:00 bash
4898 pts/0 00:00:00 ps
(二)运行过程:
程序运行到 childpid = fork();
,该进程会创建一个子进程。
若创建失败,输出错误原因并返回失败标志。
若创建成功,系统会为子进程分配资源并将父进程的相关内容全部复制到子进程中。然后fork()函数会给父进程和子进程各自返回一个返回值,使得父进程 childpid = 子进程的ID(地址)
,子进程 childpid = 0
。
此时父进程和子进程是共存的,且虚拟地址相同,它们俩会从该位置一起向下执行c程序的代码。
子进程进入 if (childpid == 0)
分支,父进程进入相应的 else
分支。
父进程执行到 wait(0)
会挂起,等待子进程结束后才会继续执行,返回值为子进程的pid。
子进程顺序执行到到返回结束。
父进程在子进程结束后继续往下执行,直到返回结束。
因此,不管子进程挂起多长时间,父进程都必须等待子进程结束后才能继续执行,这反映在输出顺序上。
五. alg.6-5-0-sleeper.c
(一)
命令行:
$ gcc -o alg.6-5-0-sleeper alg.6-5-0-sleeper.c
$ ./alg.6-5-0-sleeper
输出结果:
sleeper pid = 4940, ppid = 2081
sleeper is taking a nap for 5 seconds
sleeper wakes up and returns
命令行:
$ '/home/zhaowx9/Desktop/vscode/alg.6-5-0-sleeper' 2
输出结果:
sleeper pid = 25110, ppid = 2303
sleeper is taking a nap for 2 seconds
sleeper wakes up and returns
命令行:
$ '/home/zhaowx9/Desktop/vscode/alg.6-5-0-sleeper' 8
输出结果:
sleeper pid = 25144, ppid = 2303
sleeper is taking a nap for 8 seconds
sleeper wakes up and returns
(二)对代码中不熟悉的内容进行解析:
- int main(int argc, char* argv[]) (参考资料)
在程序启动的时候就携带参数给它,而不是运行过程中敲入东西给程序。这时候就需要用到带参数(int argc, char *argv[])的main函数。
argc: 命令行总的参数个数。一般一开始就等于1,为程序的全名。
argv[]: 保存命令行参数的字符串指针,其中第0个参数是程序的全名,以后的参数为命令行后面跟的用户输入的参数,argv参数是字符串指针数组,其各元素值为命令行中各字符串(参数均按字符串处理)的首地址。指针数组的长度即为参数个数argc。
例:
$ '/home/zhaowx9/Desktop/vscode/alg.6-5-0-sleeper' aa bb
此时
argc = 3
argv[0] = /home/zhaowx9/Desktop/vscode/alg.6-5-0-sleeper
argv[1] = aa
argv[2] = bb
- atoi() (参考资料)
函数原型:int atoi(const char *str)
函数功能:把参数 str 所指向的字符串转换为一个整数(类型为 int 型)
参数:要转换为整数的字符串
返回值:该函数返回转换后的长整数,如果没有执行有效的转换,则返回零
(三)运行过程:
初始睡眠时间设为5秒,通过设置参数1-10可以改变睡眠时间。
程序先执行第一个输出,然后睡眠相应的时间,最后执行第二个输出。
六. alg.6-5-vfork-execv-wait.c
(一)
命令行:
$ gcc -o alg.6-5-vfork-execv-wait alg.6-5-vfork-execv-wait.c
$ ./alg.6-5-vfork-execv-wait
输出结果:
This is child, pid = 5006, taking a nap for 2 seconds ...
child waking up and again execv() a sleeper: ./alg.6-5-0-sleeper.o (null)
This is parent, pid = 5005, childpid = 5006
sleeper pid = 5006, ppid = 5005
sleeper is taking a nap for 5 seconds
sleeper wakes up and returns
wait() returns childpid = 5006
(二)对代码中不熟悉的内容进行解析:
- stat() (参考资料)
使用 stat()
需要加头文件 #include <sys/stat.h>
和 #include <unistd.h>
函数原型:int stat(const char *path, struct stat *buf)
函数功能:将参数path所指的文件状态, 复制到参数buf所指的结构中
参数:文件路径(名),struct stat类型的结构体
返回值:成功返回0,失败返回-1
- execv() (参考资料)
使用 stat()
需要加头文件 #include <unistd.h>
函数原型:int execv (const char * path, char * const argv[])
函数功能:execv会停止执行当前的进程,并且以path应用进程替换被停止执行的进程,进程ID没有改变。
path: 被执行的应用程序。
argv: 传递给应用程序的参数列表,注意,这个数组的第一个参数应该是应用程序名字本身,并且最后一个参数应该为NULL,不会将多个参数合并为一个参数放入数组。
返回值:如果执行成功则函数不会返回;执行失败则直接返回-1,失败原因存于errno中。
(三)运行过程:
程序运行到 childpid = vfork();
,该进程会创建一个子进程。
若创建失败,输出错误原因并返回失败标志。
若创建成功,子进程与父进程共享地址空间。vfork()函数会给父进程和子进程各自返回一个返回值,使得父进程 childpid = 子进程的ID(地址)
,子进程 childpid = 0
。
此时子进程先于父进程运行,期间会将父进程挂起,直到execv停止执行当前子进程才会重新调度父进程。
子进程进入 if (childpid == 0)
分支,将 alg.6-5-0-sleeper.o
赋给 filename,最后 execv(filename, argv1);
停止执行当前子程序,并执行 filename 指向的程序。
通过execv得到的进程仍是子进程,且pid与原来的子进程一样,ppid为父进程的pid。
父进程在第一个子进程调用execv后继续执行,不受新生成的子进程影响,直到执行 wait(0)
,将父进程挂起,直到execv调用的程序结束,才能继续执行父进程。
execv运行的是 filename 指向的程序。如本题中就是执行 alg.6-5-0-sleeper.c
中的源代码。
七. alg.6-6-vfork-execv-nowait.c
(一)
命令行:
$ gcc -o alg.6-6-vfork-execv-nowait alg.6-6-vfork-execv-nowait.c
$ ./alg.6-6-vfork-execv-nowait
输出结果:
This is child, pid = 5035, taking a nap for 2 seconds ...
child waking up and again execv() a sleeper: ./alg.6-5-0-sleeper.o (null)
sleeper pid = 5035, ppid = 5034
sleeper is taking a nap for 5 seconds
This is parent, pid = 5034, childpid = 5035
parent calling shell ps
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 2081 2073 0 80 0 - 4877 do_wai pts/0 00:00:00 bash
0 S 1000 5034 2081 0 80 0 - 624 do_wai pts/0 00:00:00 alg.6-6-vf
0 S 1000 5035 5034 0 80 0 - 622 hrtime pts/0 00:00:00 alg.6-5-0-
0 S 1000 5036 5034 0 80 0 - 654 do_wai pts/0 00:00:00 sh
4 R 1000 5037 5036 0 80 0 - 5013 - pts/0 00:00:00 ps
命令行:
$ ps -l
输出结果:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 1000 2081 2073 0 80 0 - 4877 do_wai pts/0 00:00:00 bash
0 S 1000 5035 1382 0 80 0 - 622 hrtime pts/0 00:00:00 alg.6-5-0-
4 R 1000 5038 2081 0 80 0 - 5013 - pts/0 00:00:00 ps
命令行:
$
输出结果:
sleeper wakes up and returns
ps -q 1382
PID TTY TIME CMD
1382 ? 00:00:01 systemd
(二)对代码中不熟悉的内容进行解析:
- system() (参考资料)
函数原型:int system(const char *command)
函数功能:把 command 指定的命令名称或程序名称传给要被命令处理器执行的主机环境,并在命令完成后返回。
参数:command – 包含被请求变量名称的C字符串
返回值:如果发生错误,则返回值为-1,否则返回命令的状态。
(三)运行过程:
程序运行到 childpid = vfork();
,该进程会创建一个子进程。
若创建失败,输出错误原因并返回失败标志。
若创建成功,子进程与父进程共享地址空间。vfork()函数会给父进程和子进程各自返回一个返回值,使得父进程 childpid = 子进程的ID(地址)
,子进程 childpid = 0
。
此时子进程先于父进程运行,期间会将父进程挂起,直到execv停止执行当前子进程才会重新调度父进程。
子进程进入 if (childpid == 0)
分支,将 alg.6-5-0-sleeper.o
赋给 filename,最后 execv(filename, argv1);
停止执行当前子程序,并执行 filename 指向的程序。
通过execv得到的进程仍是子进程,且pid与原来的子进程一样,ppid为父进程的pid。
父进程在第一个子进程调用execv后继续执行,不受新生成的子进程影响,父亲进程没有 wait(0)
,因此父子进程是异步的,即父进程与execv调用的程序同时在运行。
execv运行的是 filename 指向的程序。如本题中就是执行 alg.6-5-0-sleeper.c
中的源代码。
由于父进程比子进程先结束,所以父进程结束后子进程成为孤儿,交给systemd。
实验心得
-
对进程的理解更加深入。
-
通过对示例程序中不熟悉的知识点进行攻克学习,已经能完全看懂这些代码,并对针对进程的编程有了初步的理解。
-
对示例代码的运行过程及系统调用基本理解,对其原理和实现细节也有了较为深刻的认识。