文章目录
- 进程
- 程序
- 进程
- 进程ID
- 进程表项
- C程序的启动过程
- 启动例程
- 进程终止
- atexit函数
- 示例--终止函数的执行流程以及多种进程终止方式的对比
- 进程启动和退出流程图
- 查看系统中的进程
进程
程序
-
程序是存放在磁盘文件中的可执行文件。当代码进行编辑保存后使用
gcc
等编译工具进行编译链接最后生成的可执行文件就是程序。
进程
-
程序的运行实例叫做进程(当程序运行起来就变成了进程)
可以使用指令
ps
来查看此时正在运行的hello
进程ps -elf | grep hello | grep -v grep #ps -elf: 列出系统中所有进程的详细信息。其中,-e表示显示所有进程,-l表示长格式输出,-f表示显示完整格式。 #grep hello: 从上一步的输出中筛选出包含"hello"字符串的行。 #grep -v grep: 从第二步的输出中排除包含"grep"字符串的行,以避免将grep命令本身也作为结果输出。
-
进程具有独立的权限和职责。如果系统中的某个进程崩溃,它不会影响到其余的进程。这是因为系统会给每一个进程分配一个虚拟内存,当一个进程崩溃并不会影响到别的进程的内存空间。
-
每个进程运行在其各自的虚拟地址空间中,进程之间可以通过由内核控制的机制相互通讯(进程间通信IPC)
进程ID
-
每个Linux进程都有一个唯一的数字标识符,称为进程ID,进程ID一定是非负整数。
在Linux中可以使用指令
ps
指令来查看此时后台的所有进程ps -elf | more #ps 是一个用于报告当前系统进程状态的命令。 #-elf 是 ps 命令的选项组合,其中: #-e 表示显示所有进程,包括其他用户的进程。 #-l 表示使用长格式输出,显示更详细的信息。 #-f 表示显示完整格式,包括进程的UID、PID、PPID、C、STIME、TTY、TIME、CMD等详细信息。 # | 是管道符,用于将前一个命令的输出作为后一个命令的输入。 # more 是一个用于分页显示文本内容的命令,当输出内容较多时,可以使用它来逐页查看。
这里的
PID
就是进程的标识符,而PPID
指的就是父进程的进程标识符,即创建这个进程的进程。进程标识符为1的是init
进程,它是由0号进程idle
或swapper
创建的。0号进程在系统启动时由内核自动创建,它是唯一一个没有通过fork()
或kernel_thread()
产生的进程。所以说0号进程是所有进程的祖先。
进程表项
当一个程序运行起来后,系统会给它分配一片虚拟内存空间,这片内存空间可能远远大于实际的物理内存。在虚拟内存中,用户空间占用大部分内存,这片空间存放着进程的代码段、数据段和堆栈段等等;还有一部分是内核空间,当一个进程运行的时候会在内核中生成一个进程表项用来存储这个进程的信息。例如进程操作的文件的信息、信号、还有和物理内存映射等相关信息。
在Linux系统中可以查询这个进程表项的相关信息:
首先跳转到/usr/src/linux-headers-6.5.0-41-generic/include/linux
这个目录下,然后使用grep "struct task_struct {" * -nir
查找在哪里出现了这个字段,然后打开对应的头文件查看
有关内存的结构体:
进程ID:
有关信号的结构体:
C程序的启动过程
前边讲过C程序编程可执行程序需要经过:预处理、编译、汇编、链接四个步骤,在这之后就会生成一个可执行文件C程序的启动过程。当去运行可执行文件的时候,总是从主函数开始。其实在主函数之前内核还会启动一个特殊例程就是启动例程。
启动例程
启动例程在系统中其实已经编译好了,通常放在/lib/lib32或/lib/lib64
目录下。编译器在编译时会将启动例程编译到可执行文件中去。
启动例程的作用
-
搜集命令行的参数传递给main函数中的
argc
和argv
在命令行中通常会输入以空格隔开的若干个字符串,启动例程将这些字符串的个数和内容传给main函数中的
argc
和argv
-
搜集环境信息构建环境表并传递给main函数
将环境变量
$PATH
等信息构建成一个环境表传给main函数 -
登记进程的终止函数
在每个进程结束之前都会去执行一个默认的终止函数,例如C程序在
return
之前就会去执行一个终止函数。终止函数主要是负责资源的释放,例如文件描述符的关闭、缓存的强制清空等。系统默认有一个终止函数,但是用户也同样能够自己编写一个终止函数,然后向内核去注册这个终止函数,在程序结束之前由用户去自定义释放哪些资源。
进程终止
-
正常终止
- 从main函数中返回 (
return 0
) - 调用标准C库函数 (
exit(0)
) - 调用系统调用函数
_exit
和_Exit
- 最后一个线程从启动例程中返回
- 最后一个线程调用
pthread_exit
(最后一个线程调用这个意味着所有的工作执行完毕)
- 从main函数中返回 (
-
异常终止
-
调用
abort
在C语言中有一个断言函数
assert
,它的原理就是当不满足条件时,通过调用abort()
函数来终止程序的运行,实际上abort
函数会向自身发送SIGABRT
信号,触发默认的SIGABRT
信号处理程序 -
接收到一个信号并终止
在实际开发中经常会遇到死循环的情况,这时候需要按下
Ctrl+C
或Ctrl+Z
来结束程序的运行,实际上这两个操作是发送了一个信号来终止程序的运行。分别是SIGINT
和SIGSTP
信号。 -
最后一个线程对取消请求做处理响应
-
-
进程返回
-
通常程序运行成功返回0,否则返回非0;
在实际开发中,如果成功执行的话,
return
的返回值是0反之是非0,exit
系统调用函数的传参也是一样。 -
在
shell
中可以查看进程的返回值:echo $?
-
默认每个进程终止的时候都会去调用进程终止函数,实际上可以用户可以自己定义终止函数,由用户自己去指定要释放的资源,然后通过向内核注册用户自己编写的函数就可以了。然后并不是每一种终止方式都会去调用终止函数,下边用具体的实例来演示哪些终止方式在调用的时候会去调用终止函数。
atexit函数
#include <stdlib.h>
int atexit(void (*function)(void));
//功能:允许用户注册一个函数,在main函数终止时自动被调用
//参数:一个函数指针即函数名
//返回值:如果成功执行返回0,否则返回非0
注意事项
- 可以使用多次
atexit
函数来注册多个退出处理函数,这些函数按照它们被注册的相反顺序被调用。即最后注册的函数将先被执行(执行顺序原理类似于栈) - 如果
atexit
函数注册失败,它会返回非零值。这种情况一般发生在注册了太多的退出处理函数
示例–终止函数的执行流程以及多种进程终止方式的对比
#include "header.h"
void exit_handler1(void)
{
printf("first term func1\n");
}
void exit_handler2(void)
{
printf("second term func2\n");
}
void exit_handler3(void)
{
printf("third term func3\n");
}
int main(int argc, char **argv)
{
if(argc < 3)
{
fprintf(stderr,"usage: %s [filepath] [exit_type:exit|_exit|return]\n",argv[0]);
exit(EXIT_FAILURE);
}
//向内核登记终止函数
if(atexit(exit_handler1) != 0)
{
perror("atexit");
exit(EXIT_FAILURE);
}
if(atexit(exit_handler2) != 0)
{
perror("atexit");
exit(EXIT_FAILURE);
}
if(atexit(exit_handler3) != 0)
{
perror("atexit");
exit(EXIT_FAILURE);
}
FILE *fp = fopen(argv[1],"w");
if(fp == NULL)
{
perror("fopen");
exit(EXIT_FAILURE);
}
fprintf(fp, "hello world");
if(!strcmp(argv[2],"return"))
{
return 0;
}
else if(!strcmp(argv[2],"_exit"))
{
_exit(EXIT_SUCCESS);
}
else if(!strcmp(argv[2],"exit"))
{
exit(EXIT_FAILURE);
}
else
{
fprintf(stderr,"usage: %s [filepath] [exit_type:exit|_exit|return]\n",argv[0]);
exit(EXIT_FAILURE);
}
}
通过执行结果可以发现,在通过exit
和return
作为进程的终止方式时,程序中向内核登记的终止函数都会被执行。而且它们的执行顺序以栈的方式进行执行,先登记的后执行,后登记的先执行。并且也成功地创建了文件,并向文件中写入了数据。而_exit
这个函数在它是一个系统调用函数,这里调用_exit
函数并没有去调用进程终止函数,并且也没有将缓存里的数据清空,写入到文件里。这一点需要注意,以后在使用进程终止方式的时候要选择恰当的终止方式,要不然它的结果可能和预期的结果不一样。
在代码中,向文件写入数据的方式属于全缓存,全缓存将数据写入到文件里有三种情况:1.当全缓存满的时候会将数据全部写入到文件里;2.当使用fflush
强制清空缓存也会将数据写入到文件里;3.当使用fclose
函数关闭文件的时候也会将缓存里的内容写入到文件里。(具体有关缓存的知识可以查看:标准C的IO缓存类型)这里通过exit
和return
作为进程的终止方式时,标准I/O库会检查所有打开的文件描述符,并自动将缓存区的数据写入到文件中去。也就是说fprintf
函数并不是直接将数据写入到文件里,而是先写入到缓存里,最后当使用fclose
函数关闭文件指针的时候将缓存的内容写入到文件里。而当选择_exit
作为进程的终止方式时,由于_exit
函数是一个底层的系统调用,它直接终止进程而不会执行一些高级的终止处理,包括调用注册的终止处理函数和清空缓存写入文件。
进程启动和退出流程图
如图所示:介绍一下进程的启动和退出的流程
- 首先在
main
函数调用之前,内核会调用一个启动例程,也就是这里的C start template
来负责将从命令行传入的参数的个数和参数的内容进行收集然后传给main函数中的argc argv
;然后将环境变量$PATH $SHELL
等构建成一个环境表,这个也会传给main
函数;然后会向内核登记终止函数,例如程序里的atexit
登记的三个进程终止函数。最后它会去调用main函数。 - main函数又会去调用其他的函数,在这个过程中,main函数和其他被调用的函数可以调用系统调用函数
_exit或_Exit
来终止进程的运行,由于_exit
和_Exit
都是系统调用,所以它并不会执行一些高级的处理。 - 如果main函数和其他被调用的函数执行了
exit
标准C库函数,那么它会执行一些高级操作例如以栈的运行方式去调用之前向内核登记的终止函数和清空缓存将数据写入到文件等操作。exit
函数在底层仍然是通过调用_exit或_Exit
来实现进程的退出。
查看系统中的进程
使用ps
指令可以查看当前系统中的一些进程的信息,后边加不同的参数选项能够显示不同的信息。
使用ps -ef | more
来查看当前系统中的信息
这里的UID
指的是当前运行这个进程的用户的名字,例如这里的root
超级用户或者后边的普通用户;这里的PID
是进程的唯一标识符,简称进程编号;PPID
指的是它的父进程;STIME
指的是它启动的时间;TTY
指的是它运行的终端;TIME
指的是运行它这个进程所花费的时间;CMD
指的是此进程运行的指令。
使用ps -aux|more
来查看进程占据的资源信息
这里的USER
就是进程的属主,也就是运行这个的用户;%CPU
就是它占据CPU的百分比,%MEM
是占据内存的百分比,STAT
就是当前进程的状态信息。