Linux 进程
目录
0.基本概述
- 程序概述
- 程序就是静态的一个概念。简单的说就是比如
gcc a.c -o a
那么这样就会在磁盘生成一个文件a,这个a 就是一个程序。 - 程序通常以两种面目示人。其一为源码形式,由使用编程语言(比如,C 语言)写成的一系列语句组成,是人类可以阅读的文本文件。要想执行程序,则需将源码转换为第二种形式—计算机可以理解的二进制机器语言指令。(这与脚本形成了鲜明对照,脚本是包含命令的文本文件,可以由 shell 或其他命令解释器之类的程序直接处理。)一般认为,术语“程序”的上述两种含义几近相同,因为经过编译和链接处理,会将源码转换为语义相同的二进制机器码。
- 程序就是静态的一个概念。简单的说就是比如
- 进程概述
-
进程是我们程序的一次运行活动,当我们去运行这个a 的时候,那么这个程序就跑起来了,此时系统中会多了一个进程。
-
简而言之,进程是正在执行的程序实例。 执行程序时,内核会将程序代码载入虚拟内存,
为程序变量分配空间,建立内核记账(bookkeeping)数据结构,以记录与进程有关的各种信
息(比如,进程 ID、用户 ID、组 ID 以及终止状态等)。 -
在内核看来,进程是一个个实体,内核必须在它们之间共享各种计算机资源。对于像内
存这样的受限资源来说,内核一开始会为进程分配一定数量的资源,并在进程的生命周期内,
统筹该进程和整个系统对资源的需求,对这一分配进行调整。程序终止时,内核会释放所有
此类资源,供其他进程重新使用。其他资源(如 CPU、网络带宽等)都属于可再生资源,但
必须在所有进程间平等共享。
-
- 进程内存布局
- 逻辑上将一个进程划分为以下几部分(也称为段)。
- 文本: 程序的指令。
- 数据: 程序使用的静态变量。
- 堆: 程序可从该区域动态分配额外内存。
- 栈: 随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配
存储空间
1.如何创建进程
- 进程可使用系统调用 fork() 来创建一个新进程。调用
fork()
的进程被称为父进程,新创建的进程则被称为子进程。内核通过对父进程的复制来创建子进程。子进程从父进程处继承数据段、栈段以及堆段的副本后,可以修改这些内容,不会影响父进程的“原版”内容。(在内存中被标记为只读的程序文本段则由父、子进程共享。)
头文件
#include <sys/types.h>
#include <unistd.h>
函数原型
pid_t fork(void);
返回值
-
注意,这个跟我们正常的函数不一样,正常的函数返回值都是只有一个,但是
fork()
返回值是有两个的,因为我们还创建了一个新的进程,所以是2个进程。 -
失败: 返回 -1
-
成功: 如果等于0 则是子进程 , 大于0 则是父进程.
代码示例
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t pid;
pid = fork();
if(-1 == pid)
{
perror("fork");
return -1;
}
/* 子进程代码 */
else if(0 == pid)
{
printf("我是子进程 我的进程号:%d 我的父进程号:%d\n",getpid(),getppid());
exit(1);
}
/* 父进程代码 */
else
{
printf("我是父进程 我的进程号:%d pid 的值 = :%d\n",getpid(),pid);
/* 等待子进程结束 回收它再退出 */
int status;
wait(&status);
/* 如果是正常结束 */
if(WIFEXITED(status))
{
printf("子进程正常退出。状态号:%d\n",WEXITSTATUS(status));
}
}
return 0;
}
运行结果如下
上述还用到了几个函数,下面会补充。
总结:
- fork() 有两个返回值 0是子进程 大于0是父进程
- 父进程返回的pid值 是 子进程的 ID号。
2.如何查看进程
- 命令:
ps -aux
或ps -elf
- 实际工作中,配合 grep 来查找程序中是否存在某一进程.
ps -elf | grep init
这样就能快速找到init
的 相关的进程。
- 实际工作中,配合 grep 来查找程序中是否存在某一进程.
- 命令:
top
- 类似Windows下的任务管理器
3.进程号如何获取
- 进程号和父进程号
- 每个进程都有一个进程号(PID),进程号是一个正数,用以唯一标识系统中的某个进程。对各种系统调用而言,进程号有时可以作为传入参数,有时可以作为返回值。比如,系统调用 kill() 去杀死某个进程、允许调用者向拥有特定进程号的进程发送一个信号。当需要创建一个对某进程而言唯一的标识符时,进程号就会派上用场。常见的例子是将进程号作为与进程相关文件名的一部分。系统调用
getpid()
返回调用进程的进程号。 - 每个进程都有一个创建自己的父进程。使用系统调用
getppid()
可以检索到父进程的进程号。
- 每个进程都有一个进程号(PID),进程号是一个正数,用以唯一标识系统中的某个进程。对各种系统调用而言,进程号有时可以作为传入参数,有时可以作为返回值。比如,系统调用 kill() 去杀死某个进程、允许调用者向拥有特定进程号的进程发送一个信号。当需要创建一个对某进程而言唯一的标识符时,进程号就会派上用场。常见的例子是将进程号作为与进程相关文件名的一部分。系统调用
为了程序没那么快退出,我这里整个死循环,这样可以通过命令 ps -elf
查看进程号,来查看我们用的 getpid() getppid()
函数
头文件
#include <sys/types.h>
#include <unistd.h>
函数原型
pid_t getpid(void); //获取子进程号
pid_t getppid(void); //获取父进程号
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
/*
pid_t getpid(void); //获取子进程号
pid_t getppid(void); //获取父进程号
*/
int main()
{
pid_t pid1 = getpid(); //获取子进程号
pid_t pid2 = getppid(); //获取父进程号
printf("子进程号:%d\n",pid1);
printf("父进程号:%d\n",pid2);
while(1);//这里方便查看设置死循环 可通过命令 ps -elf 查看进程号
return 0;
}
运行结果
也可通过命令 ps -elf
查看进程号
4. 僵尸进程和孤儿进程
- 正常终止 、 异常终止。
- 进程运行中需要消耗系统资源(内存、IO),进程终止得完全释放这些资源。
4.1 僵尸进程
- 子进程比父进程先结束。这时候如果父进程还没有执行
wait()
, 那么子进程就是僵尸进程。 - 因为当子进程结束后,内核会将子进程转为僵尸进程(zombie)来处理。会把子进程的大部分资源释放,给其他进程重新使用,唯一保留下的是内核的进程表中的一条记录。这个进程表中其中有 子进程的ID、终止状态、资源使用数据等信息。
- 操作系统已经释放了大部分资源,剩下的其实是需要他的父进程去回收资源的,所以我们一般会用
wait()
或waitpid()
去回收子进程,这才是得到完全的释放。 - 如果父进程执行了
wait()
那么这个时候就不需要子进程最后的一些信息,内核就会删除僵尸进程。 - 一般也就是分为下面几种
- 子进程先终止了,父进程没有调用
wait()
并且父进程还在,那么这就是僵尸进程,这个父一直不死,那么子进程就一直是僵尸进程。 - 子进程先终止了,父进程调用了 wait() ,在子进程退出到在父进程调用 wait() 之前,这段也是僵尸进程了。不过最后父进程执行了
wait()
回收了子进程。 - 子进程先终止了,如果父进程没有执行 wait() ,然后父进程结束进程了,那么子进程将会让 init 进程(祖先进程)接管,
init
进程自动调用wait()
去回收。
- 子进程先终止了,父进程没有调用
- 注意,如果子进程先终止,父进程还没终止,父进程也没调用
wait()
,子进程变成僵尸进程,你用SIGKILL
信号都是杀不死的。那么解决办法就是让它的父进程终止。下图是演示僵尸进程是杀不死的。
4.2 孤儿进程
- 父进程比子进程先终止,子进程就变成了孤儿进程,通俗点说就是孩子它爸先不在了,它就成孤儿了。称为孤儿进程。
- 子进程没有了爸爸,那
init进程
会收养它,init进程
则担起了回收子进程的责任。
5.如何避免僵尸进程
- 主要想到的就是下面的几种。
- 调用
wait()
或waitpid()
去回收子进程。 - 利用
SIGCHLD
信号 - 在子进程终止后,如果没有回收,可以
杀死它的父进程
,让它的父进程结束。这也是一种办法。
- 调用
函数详细介绍在下面
6. wait和waitpid介绍
用这两个需要包含头文件
#include <sys/types.h>
#include <sys/wait.h>
6.1 wait() 函数
- 原型:
pid_t wait(int *wstatus);
-
参数
- 保存被回收的进程退出时的状态,即我们用的return 或 exit 退出码。如果你不关心这个可以设置成
NULL
- 保存被回收的进程退出时的状态,即我们用的return 或 exit 退出码。如果你不关心这个可以设置成
-
返回值
- 成功: 返回被回收的进程的
ID号
- 失败: 返回 -1,
errno
会置为ECHILD
- 成功: 返回被回收的进程的
进程一旦调用了 wait() ,就会阻塞状态,由 wait() 分析当前进程的某个子进程是否已经退出 (因为一个进程可能有多个子进程),如果有一其中一个退出了,wait就会收集这个子进程的一些消息,然后销毁后返回信息,如果一直都没有子进程终止,那该进程会一直阻塞状态,直到有一个子进程终止为止。
可以用下方的宏来判断子进程的退出状态
宏定义 | 描述 |
---|---|
WIFEXITED(status) | 如果子进程正常结束,返回非0值 |
WEXITSTATUS(status) | 如果WIFEXITED非0,也就是正常退出,返回子进程退出码 |
WIFSIGNALED(status) | 子进程如果是因为捕获信号而终止,返回非0值 |
WTERMSIG(status) | 如果WIFSIGNALED非0,返回信号代码 |
WIFSTOPPED(status) | 如果子进程被暂停,返回非0值 |
WSTOPSIG(status) | 如果WIFSTOPPED非0,返回信号代码 |
6.2 waitpid() 函数
- 原型
pid_t waitpid(pid_t pid, int *wstatus, int options);
- 参数
- pid:
- pid 大于 0: 等待进程ID为pid 的子进程。
- pid 等于 0: 等待与调用进程(父进程)同一个进程组(process group)的所有子进程。
- pid 小于 -1: 等待进程组标识符与 pid 绝对值相等的所有子进程。
- pid 等于 -1: 等待任意进程,和
wait(&status)和waitpid(-1,&status,0)一样
- status:
- 保存被回收进程的状态信息
- options:
-
WNOHANG
- 如果参数pid所指的子进程没有结束,则立马返回0,不会阻塞。
- 如果参数pid所指的子进程没有,则waitpid 报错,错误号为ECHILD 通过perror()输出提示
No child processes
。
-
WUNTRACED
- 除了返回终止子进程的信息外,还返回因信号而停止的子进程信息
-
WCONTINUED
- 返回因收到
SIGCONT 信号
而恢复执行的已停止子进程的状态信息。
- 返回因收到
-
- pid:
6.3 wait和waitpid中的wstatus
wait和waitpid 中的 wstatus 可以以下区分子进程事件
- 子进程是用exit() 或 _exit() 结束,并指定整数值退出,如子进程用exit(1);结束,那么wstatus 就是 1
- 子进程收到未处理的信号而结束
- 子进程因为信号而停止,并以
WUNTRACED
标志调用了waitpid()
- 子进程收到
信号 SIGCONT
而恢复运行,并以WCONTINUED
标志调用了waitpid()。
6.4 SIGCHLD信号
- 上面有讲到回收子进程办法有 wait 和 waitpid 回收,如果子进程还没结束,会进行阻塞。(当然waitpid也可以设置不阻塞,就不断的去检测子进程是否结束) ,阻塞就啥也干不了,轮询也消耗cpu资源。还有一种办法就是 通过
SIGCHLD 信号
去处理。 - 子进程结束,系统都会向其父进程发送
SIGCHLD 信号
。 - 在父进程代码中加上代码
signal(SIGCHLD, SIG_IGN);
表示告诉内核对子进程的结束我不关心,子进程结束就结束,我不管。
7. 进程是否有上限呢?
- 在 32 位平台中, pid_max 文件的最大值为 32768。
- 在 64 位平台中,该文件的最大值可以高达到 2^22 (4194304)400多万个。
- 系统特有的
/proc/sys/kernel/pid_max
文件来进行调整(其值=最大进程号+1)。