DJ2-3 进程管理
创始人
2025-05-28 13:39:14
0

目录

2.3.6  Linux 的进程控制 1

0. 进程的创建方式

1. 进程的创建

2. 进程的创建之代码实现

3. 进程在内存空间的布局

4. 父子进程的主要异同

5. 父子进程的应用

2.3.6  Linux 的进程控制 2

1. 进程的退出

2. 进程的等待与睡眠

3. wait() 和 waitpid()

4. 进程的执行


2.3.6  Linux 的进程控制 1

0. 进程的创建方式

Unix 和 Linux 中创建进程的方式:

1)在 shell 中执行命令或可执行文件:由 shell 进程调用 fork() 创建子进程。

2)在代码中(已经存在的进程中)调用 fork() 创建子进程。

  • fork() 创建的进程为子进程
  • 原进程为父进程

使用 pstree 命令查看进程树

1)Linux系统中进程 0(PID=0)是由内核创建的,而其它所有进程都是由父进程调用 fork() 创建的。

2)Linux系统中进程 0 在创建子进程(PID=1,init 进程)后,进程 0 就转为交换进程或空闲进程。

3)进程1(init 进程)是系统中其它所有进程的共同祖先。

Linux 终端输入命令:

$ pstree

查看进程树如图所示,为什么我的进程 1 名字是 systemd(?)

1. 进程的创建

1)函数原型

  • #include
  • #include
  • pid_t fork(void);

2)返回值

① fork() 调用正确将会在子进程中和父进程中分别返回。

  • 在子进程中返回值为 0(不合法的 PID,提示当前运行在子进程中)
  • 在父进程中返回值为子进程 ID(让父进程掌握所创建子进程的 ID 号)

② fork() 调用出错返回 -1 。

2. 进程的创建之代码实现

调用 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 值不同。可见,父进程和子进程没有共享同一份数据。

3. 进程在内存空间的布局

这里的程序当然指的是程序段和数据段。

1)命令行参数和环境变量

主要用于支撑函数调用,如:存放参数、存放局部变量等。

2)堆栈:用于动态分配内存。

3)未初始化的数据

程序执行之前,将此段中的数据初始化为 0,典型应用为定义数组。如:设置全局变量

long sum[1000];

回收内存时并不会抹去其中的数据,因此需要在下次分配出去时对其进行初始化。

4)初始化的数据

包含了程序中需明确赋初值的变量,如:设置全局变量

int maxcount = 99;

5)正文

是指 CPU 执行的代码部分,通常是共享的、只读的。

磁盘中的程序到内存中的进程的映射。

4. 父子进程的主要异同

父子进程的相同处父子进程的不同处
  1. 真实用户 ID,真实组 ID
  2. 有效用户 ID,有效组 ID
  3. 环境变量
  4. 打开的文件
  1. fork() 的返回值
  2. 子进程 ID 和父进程 ID
  3. 子进程时间戳被设置为 0

时间戳:tms_utime,tms_stime,tms_cutime,tms_ustime 。

5. 父子进程的应用

父进程希望通过复制自己,即共享代码和复制数据空间,来使自己和子进程能够同时执行相同代码中的不同分支。

  1. 在网络并发服务器中,父进程等待客户端的服务请求;
  2. 当请求达到时,父进程调用 fork() 创建子进程处理该请求;
  3. 而父进程继续等待下一个服务请求。

2.3.6  Linux 的进程控制 2

1. 进程的退出

Linux 下进程的退出方式分为正常退出和异常退出两种。

1)正常退出的三种方式

  • 在 main 函数中执行 return
  • 调用 exit 函数:void exit(int __status)
  • 调用 _exit 函数

2)异常退出的两种方式

  • 调用 abort 函数
  • 进程收到某个信号,而该信号使进程终止

几种方式的区别

1)exit 是一个函数,执行完后把控制权交给系统。

2)return 是函数执行完后的返回,执行完后把控制权交给调用函数。

3)_exit 执行完后立即将控制权交给内核;exit 执行完后要先执行一些清除操作(如:处理句柄),然后才将控制权交给内核。

exit 函数要检查文件的打开情况,把文件缓冲区的内容写回文件。

2. 进程的等待与睡眠

在 Unix/Linux 系统中,正常情况下,子进程是通过父进程创建的,且两者的运行是相互独立的,父进程永远无法预测子进程到底什么时候结束。

当一个进程调用 exit() 结束自己的生命时,其实它并没有真正的被销毁,内核只是释放了该进程的所有资源,包括打开的文件、占用的内存等。但是留下了一个称为僵尸进程的数据结构,这个结构保留了一定的信息,包括进程号PID、退出状态、运行时间。这些信息只有直到父进程通过 wait() 或 waitpid() 来获取时才会释放。

这样设计的主要目的是保证只要父进程想知道子进程结束时的状态信息,就可以得到。

1)僵尸进程:一个进程使用 fork() 创建子进程,如果子进程退出,而父进程并没有调用 wait() 或 waitpid() 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中,这种进程称之为僵尸进程。

2)孤儿进程:一个父进程退出,而它的一个或多个子进程还在运行,那么这些子进程将会成为孤儿进程。孤儿进程将被 init 进程(进程号为 1)所收养,并由 init 进程对它们完成状态收集工作。

3. wait() 和 waitpid()

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

4. 进程的执行

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 系列函数。

  • l:表示 list,表示命令行参数采用列表形式。
  • v:表示 vector,表示命令行参数采用数组形式。
  • e:表示由函数调用者提供环境变量表。
  • p:表示通过环境变量 PATH 来指定路径,查找可执行文件。

使用场景

在 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 。

相关内容

热门资讯

Redis KV如何存储 一、KV如何存储? 为了实现从键到值的快速访问,Redis 使用了一个哈...
Java if else分支结... Java 支持两种选择语句:if 语句和 switch 语句。其中 if 语句使用布尔...
与会人士高度评价国际调解院 点... 与会人士高度评价国际调解院 点赞国际法治领域创新之举 新华社香港5月30日电 题:与会人士高度评价...
还在stream中使用peek... 文章目录简介peek的定义和基本使用peek的流式处理Stream的懒执行策略peek为什么只被推荐...
C++析构函数详解 创建对象时系统会自动调用构造函数进行初始化工作,同样,销毁对象时系统也会...
Apache Paimon 二、典型实践应用 2.1)离线数仓加速 离线数仓加速可以说是一个非常典型的应用场景了 ...
“内鬼”李卓勋,任上被查 据“清廉龙江”5月30日消息,黑龙江省哈尔滨市纪委监委第九审查调查室主任李卓勋涉嫌严重违纪违法,目前...
津巴布韦专家:美关税政策对自身... 连日来,非洲各界人士表示,美国近期推动的一系列关税政策不仅对自身,而且对非洲都将造成影响,大多非洲国...
顾客不惜排队三小时,济南这个小... 端午节前,济南的老街巷已提前进入“节日模式”。午后十二点半,在济南制锦市小区的乐安街中段,一家名为“...
美媒为劝特朗普,搬出了钱学森 特朗普政府再次向在美中国留学生下手,遭批“新学术冷战”。 对于美国《纽约时报》专栏作者凯瑟琳·金斯伯...