目录
2.3.6 Linux 的进程控制 1
0. 进程的创建方式
1. 进程的创建
2. 进程的创建之代码实现
3. 进程在内存空间的布局
4. 父子进程的主要异同
5. 父子进程的应用
2.3.6 Linux 的进程控制 2
1. 进程的退出
2. 进程的等待与睡眠
3. wait() 和 waitpid()
4. 进程的执行
Unix 和 Linux 中创建进程的方式:
1)在 shell 中执行命令或可执行文件:由 shell 进程调用 fork() 创建子进程。
2)在代码中(已经存在的进程中)调用 fork() 创建子进程。
使用 pstree 命令查看进程树
1)Linux系统中进程 0(PID=0)是由内核创建的,而其它所有进程都是由父进程调用 fork() 创建的。
2)Linux系统中进程 0 在创建子进程(PID=1,init 进程)后,进程 0 就转为交换进程或空闲进程。
3)进程1(init 进程)是系统中其它所有进程的共同祖先。
Linux 终端输入命令:
$ pstree
查看进程树如图所示,为什么我的进程 1 名字是 systemd(?)
1)函数原型
2)返回值
① fork() 调用正确将会在子进程中和父进程中分别返回。
② fork() 调用出错返回 -1 。
调用 fork() 的目的是复制进程自身,使父子进程能同时执行不同分支的代码。
注意的一点:调用 fork() 之后,一定是父子进程同时执行 fork() 之后的代码,而之前的代码已经由父进程执行完毕。
示例 1
#include
/* 需要包含的头文件 */
#include
#include int main(void) {pid_t pid;pid = fork(); /* 创建子进程 */if (pid == -1) /* 调用出错 */printf("fork error \n");else if (pid == 0) { /* 调用正常,在子程序中 */printf("the returned value is % d\n", pid);printf("In child process !!\n");printf("My PID is % d\n", getpid());} else { /* 调用正常,在父程序中 */printf("the returned value is % d\n", pid);printf("In father process !!\n");printf("My PID is %d\n", getpid());}return 0;
}
第一次输出结果:子进程 pid 为 6940,父进程 pid 为 6939 。
$ ./use_fork
the returned value is 6940
In father process !!
My PID is 6939
the returned value is 0
In child process !!
My PID is 6940
第二次输出结果:子进程 pid 为 6991,父进程 pid 为 6992 。
$ ./use_fork
the returned value is 6992
In father process !!
My PID is 6991
the returned value is 0
In child process !!
My PID is 6992
可见,每次进程的 pid 都是不一样的。
示例 2
#include
/* 需要包含的头文件 */
#include
#include int main() {int a = 5, b = 2;pid_t pid;pid = fork();if (pid == 0) { /* 调用正常,在子程序中 */a = a - 4;printf("I'm a child process wirh PID [%d], ""the value of a: %d, the value of b: %d.\n",pid, a, b);} else if (pid < 0) { /* 调用出错 */perror("fork");} else { /* 调用正常,在父程序中 */sleep(1); /* 实测父进程较快,因此让它睡眠一下 */printf("I'm a parent process wirh PID [%d], ""the value of a: %d, the value of b: %d.\n",pid, a, b);}return 0;
}
输出结果:
$ ./use_fork
I'm a child process wirh PID [0], the value of a: 1, the value of b: 2.
I'm a parent process wirh PID [8333], the value of a: 5, the value of b: 2.
父进程和子进程输出的 a 值不同。可见,父进程和子进程没有共享同一份数据。
这里的程序当然指的是程序段和数据段。
1)命令行参数和环境变量
主要用于支撑函数调用,如:存放参数、存放局部变量等。
2)堆栈:用于动态分配内存。
3)未初始化的数据
程序执行之前,将此段中的数据初始化为 0,典型应用为定义数组。如:设置全局变量
long sum[1000];
回收内存时并不会抹去其中的数据,因此需要在下次分配出去时对其进行初始化。
4)初始化的数据
包含了程序中需明确赋初值的变量,如:设置全局变量
int maxcount = 99;
5)正文
是指 CPU 执行的代码部分,通常是共享的、只读的。
磁盘中的程序到内存中的进程的映射。
父子进程的相同处 | 父子进程的不同处 |
---|---|
|
|
时间戳:tms_utime,tms_stime,tms_cutime,tms_ustime 。
父进程希望通过复制自己,即共享代码和复制数据空间,来使自己和子进程能够同时执行相同代码中的不同分支。
Linux 下进程的退出方式分为正常退出和异常退出两种。
1)正常退出的三种方式
2)异常退出的两种方式
几种方式的区别
1)exit 是一个函数,执行完后把控制权交给系统。
2)return 是函数执行完后的返回,执行完后把控制权交给调用函数。
3)_exit 执行完后立即将控制权交给内核;exit 执行完后要先执行一些清除操作(如:处理句柄),然后才将控制权交给内核。
exit 函数要检查文件的打开情况,把文件缓冲区的内容写回文件。
在 Unix/Linux 系统中,正常情况下,子进程是通过父进程创建的,且两者的运行是相互独立的,父进程永远无法预测子进程到底什么时候结束。
当一个进程调用 exit() 结束自己的生命时,其实它并没有真正的被销毁,内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等。但是留下了一个称为僵尸进程的数据结构,这个结构保留了一定的信息,包括进程号PID、退出状态、运行时间。这些信息只有直到父进程通过 wait() 或 waitpid() 来获取时才会释放。
这样设计的主要目的是保证只要父进程想知道子进程结束时的状态信息,就可以得到。
1)僵尸进程:一个进程使用 fork() 创建子进程,如果子进程退出,而父进程并没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。
2)孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将会成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。
1)父进程一旦调用了 wait() 就立即阻塞自己,由 wait() 来自动分析当前进程的某个子进程是否已经退出。
2)如果 wait() 找到了变成僵尸的子进程,就会收集这个子进程的信息,并在把它彻底销毁后返回;如果没有找到变成僵尸的子进程,wait() 就会让父进程继续阻塞在这里,直到有僵尸子进程出现为止。
pid_t wait(int *__stat_loc);
函数说明:wait() 会暂时停止目前进程的执行, 直到有信号来到或子进程结束。如果在调用 wait() 时子进程已经结束,则 wait() 会立即返回子进程结束状态值。子进程的结束状态值会由参数 status 返回,而子进程的进程识别码也会一起返回。如果不在意结束状态值,则参数 status 可以设成 NULL 。
waitpid() 和 wait() 的作用是完全相同的,但 waitpid() 多出了两个可由用户控制的参数 pid 和 options 。
waitpid() 可等待一个特定的进程,而 wait() 则返回任意一个终止子进程的状态。
waitpid() 提供了一个 wait() 的未阻塞版本。当用户希望取得一个子进程的状态,但不想阻塞时,可使用 waitpid() 。
示例
#include
/* 需要包含的头文件 */
#include
#include
#include
#include
#include void waitprocess() {int count = 0;pid_t pid = fork();int status = -1;if (pid < 0) {printf("fork error for %d\n", errno);} else if (pid > 0) {printf("this is parent, pid = %d, ppid = %d\n", getpid(), getppid());wait(&status);} else {printf("this is child, pid = %d, ppid = %d\n", getpid(), getppid());int i;for (i = 0; i < 10; ++i) {count++;sleep(1);printf("count = %d\n", count);}exit(5); /* 参数作为程序退出的返回值 */}printf("child exit status id %d\n", WEXITSTATUS(status));/* WIFEXITED(status) 这个宏用来指出子进程是否为正常退出的,如果是则返回一个非零值。当 WIFEXITED 返回非零值时,我们可以用这个宏来提取子进程的返回值。如果子进程调用 exit(5) 退出,则 WEXITSTATUS(status) 就会返回 5 。请注意,如果进程不是正常退出的,也就是说,WIFEXITED 返回 0,这个值就毫无意义。*/printf("end of program from pid = %d\n", getpid());
}int main() {waitprocess();return 0;
}
输出结果:
说明:ppid 是该进程父进程的 pid 。可见,最后退出的是父进程。
$ ./use_wait
this is parent, pid = 11973, ppid = 6020
this is child, pid = 11974, ppid = 11973
count = 1
count = 2
count = 3
count = 4
count = 5
count = 6
count = 7
count = 8
count = 9
count = 10
child exit status id 5
end of program from pid = 11973
exec 函数簇:根据指定的文件名或目录名找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。进程程序替换:正在执行的进程本身的 PCB 不会发生改变,仅仅用新的程序段和数据段替换原来的程序段和数据段。可见,进程程序替换是不会创建一个新进程的。
exec 函数簇原型
#include
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
六个函数开头均为 exec,所以称为 exec 系列函数。
使用场景
在 Linux 中使用 exec 函数簇主要有以下两种情况。
1)当进程认为自己不能再为系统和用户做出任何贡献时,就可以调用任一 exec 函数簇让自己重生。
2)如果一个进程想执行另一个程序,那么它就可以调用 fork() 新建一个进程,然后调用 exec 函数簇使子进程重生。
示例
#include
/* 需要包含的头文件 */
#include int main() {printf("entering main process---\n");if (fork() == 0) {execl("/bin/ls", "ls", "-l", NULL);printf("entering main process---\n");}return 0;
}
输出结果:由于在第二次打印之前,execl 已经把程序替换了。即不再执行 main 函数的程序,而是变成执行 /bin/ls 目录下的 ls 命令程序。因此在输出中,我们看不到第二次打印结果。
$ ./use_execl
entering main process---
$ 总用量 28
-rw-rw-r-- 1 envoutante envoutante 138 3月 15 16:36 Makefile
-rwxrwxr-x 1 envoutante envoutante 17400 3月 15 16:47 use_execl
-rw-rw-r-- 1 envoutante envoutante 262 3月 15 16:47 use_execl.c
^C
这个好像无法自己退出,所以手动了一个 ctrl + c 。