达者为先 师者之意
文章目录
1 进程的概述
Linux系统编程——进程
进程是程序的动态的执行过程。程序是静态的过程
程序: 存储在磁盘上的一堆指令。
进程: 将程序动态的运行起来,就转成进程,(即程序的执行过程)过程: 包含进程的创建,进程的运行以及进程消亡 。
- Linux系统是一个多进程的系统,它的进程之间具有互不干扰等行性、特点。也就是说,进程之间是分离的任务,拥有各自的权利和责任。其中,每个进程都运行在各自独立的虚拟地址空间,因此,即使一个进程发生了异常,它也不会影响到系统的其他进程。
- 当创建一个进程,系统会分配0—4G 虚拟地址空间,其中0—3G是用户空间 3—4G内核空间 。多个进程共用同一份内核, 各自进程的用户空间是独立的 。
- 空间的划分是由内核来决定,0—3G用户空间3—4
内核空间,这个划分是内核默认 。
- 代码段: 存放字符常量
- 数据段: 初始化后的全局变量 只读数据段: const 所修饰的变量
- bss数据段: 未被初始化的全局变量
- 堆:程序员自己手动申请的空间 malloc -------- 小栈 大堆 思想 。
- 栈:存放函数内部所包含的局部变量以及函数。
进程的类型:
- 交互进程 :shell 终端 就是交互进程 。
- 批处理进程 :通过 ps –aux 来进行查看到的进程 。
- 守护进程 :随系统的启动开始运行,系统关闭的时候,结束运行,在运行期间不会停止运行。在linux 操作系统下面守护进程不受终端的控制 。
2 ps命令
ps 命令是最常用的监控进程的命令,通过此命令可以查看系统中所有运行进程的详细信息。
ps 命令有多种不同的使用方法,这常常给初学者带来困惑。在各种 Linux 论坛上,询问 ps 命令语法的帖子屡见不鲜,而出现这样的情况,还要归咎于 UNIX 悠久的历史和庞大的派系。在不同的 Linux 发行版上,ps 命令的语法各不相同。为此,Linux 采取了一个折中的方法,即融合各种不同的风格,兼顾那些已经习惯了其它系统上使用 ps 命令的用户。
ps 命令的基本格式如下:
[root@localhost ~] # ps aux
#查看系统中所有的进程,使用 BS 操作系统格式
[root@localhost ~] # ps -le
#查看系统中所有的进程,使用 Linux 标准命令格式
选项:
- a:显示一个终端的所有进程,除会话引线外;
- u:显示进程的归属用户及内存的使用情况;
- x:显示没有控制终端的进程;
- -l:长格式显示更加详细的信息;
- -e:显示所有进程;
可以看到,ps 命令有些与众不同,它的部分选项不能加入"-“,比如命令"ps aux”,其中"aux"是选项,但是前面不能带“-”。
大家如果执行 “man ps” 命令,则会发现 ps 命令的帮助为了适应不同的类 UNIX 系统,可用格式非常多,不方便记忆。所以,我建议大家记忆几个固定选项即可。比如:
-
“ps aux” 可以查看系统中所有的进程;
-
“ps -le” 可以查看系统中所有的进程,而且还能看到进程的父进程的 PID 和进程优先级;
-
“ps -l” 只能看到当前 Shell 产生的进程;
-
注意:还可以用 grep [选项] [文件或目录]…过滤/搜索的特定字符
如"ps aux|grep init" 查找 init 字段的进程
有这三个命令就足够了,下面分别来查看。
表1 罗列出了以上输出信息中各列的具体含义
"ps aux"命令可以看到系统中所有的进程,"ps -le"命令也能看到系统中所有的进程。由于 “-l” 选项的作用,所以 “ps -le” 命令能够看到更加详细的信息,比如父进程的 PID、优先级等。但是这两个命令的基本作用是一致的,掌握其中一个就足够了。
表 2 罗列出以上输出信息中各列的含义
如果不想看到所有的进程,只想查看一下当前登录产生了哪些进程,那只需使用 “ps -l” 命令就足够了:
可以看到,这次从 pts/1 虚拟终端登录,只产生了两个进程:一个是登录之后生成的 Shell,也就是 bash;另一个是正在执行的 ps 命令。
3 top命令
ps 命令可以一次性给出当前系统中进程状态,但使用此方式得到的信息缺乏时效性,并且,如果管理员需要实时监控进程运行情况,就必须不停地执行 ps 命令,这显然是缺乏效率的。
为此,Linux 提供了 top 命令。top 命令可以动态地持续监听进程地运行状态,与此同时,该命令还提供了一个交互界面,用户可以根据需要,人性化地定制自己的输出,进而更清楚地了进程的运行状态。
top 命令的基本格式如下:
[root@localhost ~] #top [选项]
选项:
- -d 秒数:指定 top 命令每隔几秒更新。默认是 3 秒;
- -b:使用批处理模式输出。一般和"-n"选项合用,用于把 top 命令重定向到文件中;
- -n 次数:指定 top 命令执行的次数。一般和"-"选项合用;
- -p 进程PID:仅查看指定 ID 的进程;
- -s:使 top 命令在安全模式中运行,避免在交互模式中出现错误;
- -u 用户名:只监听某个用户的进程;
在 top 命令的显示窗口中,还可以使用如下按键,进行一下交互操作:
- ? 或 h:显示交互模式的帮助;
- P:按照 CPU 的使用率排序,默认就是此选项;
- M:按照内存的使用率排序;
- N:按照 PID 排序;
- T:按照 CPU 的累积运算时间排序,也就是按照 TIME+ 项排序;
- k:按照 PID 给予某个进程一个信号。一般用于中止某个进程,信号 9 是强制中止的信号;
- r:按照 PID 给某个进程重设优先级(Nice)值;
- q:退出 top 命令;
4 进程标识码
函数原型:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);
pid_t getppid(void);
pid_t 的实质是int类型的。
getpid()返回当前进程标识码,getppid()返回父进程标识。
5 fork与vfork函数
fork函数
我们都知道fork可以用于进程的创建,那首先我们来了解一下fork函数原型和基本的两种用法才真正的了解我们何时才会用到fork来进行进程的创建。
函数原型:
#include<unistd.h>
pid_t fork(void);
参数含义:无参传入,返回pid_t类型的数值。pid_t 的实质是int类型的。
fork函数被调用一次,但是返回两次
子进程返回的是0,而父进程返回值则是新子进程的进程ID。
注意:父进程返回值是新进程的进程ID
因为一个进程的子进程可以有多个,并且没有一个函数是一个进程可以获得其所有子进程的进程ID。父进程中没有记录子进程的pid。所以为了方便父进程知道和处理子进程,fork()返回最新子进程的pid。
用法一
- 一个父进程希望复制自己,使父、子进程同时执行不同的代码段。
这在网络服务进程中是最常见的一种处理,父进程等待客户端的服务请求,当达到请求到达时,父进程调用fork,使子进程处理此请求。父进程则等待下一个服务请求到达。
用法二
- 一个进程要执行一个不同的程序
这个用法对shell是常见的情况。在这种情况下,子进程从fork返回后立即调用exec。
vfork函数(尽量不要使用)
函数原型:
#include<unistd.h>
pid_t vfork(void) // 返回值pid_t 的实质是int类型的
- 不进行空间拷贝(写时都不拷贝),子进程pcb直接指向父进程pcb指向的位置
- 父进程一定是等到子进程执行完毕以后在执行
- 必须调用exit 或exec系列的函数,否则就会出现段错误 4、任何一个系统上实现的vfork或多过少的都有问题,尽量 不要使用
vfork()注意:
子进程先执行,不能return返回,return是在堆栈上进行操作,return返回相当于压栈一次,出栈两次,即子进程释放了main的栈,父进程再去释放就形成了段错误。
总结:fork和vfork的区别
- 调用fork后, 父子进程运行顺序不定,而vfork子进程一定先运行。
- fork存在写时拷贝的机制,子进程拷贝父进程地址空间。 vfork子进程(在调用exec(进程替换)或者exit之前)共享父进程的地址空间(一改都改)。
- vfork必须调用exit返回,否则会引发段错误。
6 进程的退出
Linux 下进程的退出分为正常退出和异常退出两种:
- 正常退出
a. 在main()函数中执行return 。
b.调用exit()函数
c.调用_exit()函数 - 异常退出
a.调用abort函数
b.进程收到某个信号,而该信号使程序终止,比如Ctrl+c,进程收到某个信号,而该信号是程序中止。
但不管是哪种退出方式,系统最终都会执行内核中的某一代码。这段代码用来关闭进程所用已打开的文件描述符,释放它所占用的内存和其他资源。
几种退出方式的比较
-
exit和return 的区别:
exit是一个函数,有参数。exit执行完后把控制权交给系统
return是函数执行完后的返回。renturn执行完后把控制权交给调用函数。 -
exit和abort的区别:
exit是正常终止进程
abort是异常终止。
关于exit函数
函数原型
#include <stdlib.h>
void exit(int status);
exit函数的参数:当前进程要返回的退出码
注意:status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其它的值表示出现了错误,进程非正常结束。
函数说明
- 直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构。exit(0)表示正常退出,exit(x)(x不为0)表示异常退出,这个x是返回给操作系统(包括UNIX,Linux,和MS DOS)的,以供其他程序使用。
关于_exit函数
函数原型
#include <unistd.h>
void _exit(int status)
注意:_exit 函数的参数:当前进程要返回的退出码
status是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0表示正常结束;其它的值表示出现了错误,进程非正常结束。
_Exit函数等效于 _exit函数。
函数说明
- _exit函数会立即终止调用过程。属于该进程的任何打开的文件描述符都被关闭;进程的任何子进程都由init进程(初始化进程,进程ID:1)继承,进程的父进程将被发送一个SIGCHLD信号。值状态作为进程的退出状态返回给父进程,并且可以使用wait(2)系列调用之一收集。
exit()和_exit()函数的区别
exit和_exit函数都是用来终止进程的。当程序执行到exit或_exit时,系统无条件的停止剩下所有操作,清除各种数据结构,并终止本进程的运行。
- exit在头文件stdlib.h中声明,而_exit()声明在头文件unistd.h中声明。 exit中的参数exit_code为0代表进程正常终止,若为其他值表示程序执行过程中有错误发生。
- _exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
- 调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin, stdout, stderr …). exit函数是在_exit函数之上的一个封装,其会调用_exit,并在调用之前先刷新流。
- exit()函数与_exit()函数最大区别就在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。由于Linux的标准函数库中,有一种被称作“缓冲I/O”的操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续的读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区读取;同样,每次写文件的时候也仅仅是写入内存的缓冲区,等满足了一定的条件(如达到了一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也给编程代来了一点儿麻烦。比如有一些数据,认为已经写入了文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit()函数直接将进程关闭,缓冲区的数据就会丢失。因此,要想保证数据的完整性,就一定要使用exit()函数。
exit 最后也会调用 _exit函数,但exit在中止进程的同时,还会刷新缓冲区、关闭流等
如下图:
7 进程等待
前面提到这么一个问题
父进程在忙,子进程结束了,但无人回收,这样就造成了“死亡”的子进程一直占用资源,这个时候的子进程被称为“僵尸进程”
为了解决这个问题,最初的思路是:让父进程停下,等待子进程执行完,然后回收子进程,清理掉的子进程ID。
进程等待的必要性
-
子进程退出,父进程如果不管不顾,就可能造成“僵尸进程”的问题,进而造成内存泄漏。
-
进程一旦变成僵尸状态,就会刀枪不入,kill -9 强杀也无能为力,
-
父进程派给子进程的任务完成的如何,父进程得知道,
-
父进程通过进程等待的方式,回收子进程的资源,获取子进程的退出信息。
注意 : 当子进程执行时, wait()可以暂停父进程的执行,使其等待。一旦子进程执行完,等待的父进程就会重新执行。如果有多个子进程在执行,那么父进程中的 wait()在第一个子进程结束时返回,恢复父进程执行。
进程等待的方法(如何让父进程进行进程等待):
wait函数和waitpid函数
wait函数:
函数原型
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait (int* status);
返回值 pid_t : 成功返回被等待进程的 PID,失败返回 -1。
参数 status: 参数是一个指针类型,但是该指针类型并不是要传递一个指针参数,而是一个输出型参数 ; 获取子进程的退出状态,不关心则可以设置为 NULL。调用函数时不用传入具体的值,在函数内部赋予值,在函数返回之后,调用者就可以拿到函数当中赋予的值。
子进程的结束状态返回后存于status,底下有几个宏可判别结束情况
- WIFEXITED(status)如果子进程正常结束则为非0值。
- WEXITSTATUS(status)取得子进程exit()返回的结束代码,一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
- WIFSIGNALED(status)如果子进程是因为信号而结束则此宏值为真
- WTERMSIG(status)取得子进程因信号而中止的信号代码,一般会先用 WIFSIGNALED 来判断后才使用此宏。
- WIFSTOPPED(status)如果子进程处于暂停执行情况则此宏值为真。一般只有使用WUNTRACED 时才会有此情况。
- WSTOPSIG(status)取得引发子进程暂停的信号代码。
wait函数的四个特性:
1.输出型参数,与其对应的有:
- 输入型参数→调用者给被调用函数传参;
- 输入输出型参数;
2.int* status是一个指针类型占四个字节,但是实际中只使用到后两个字节,将这两个字节分为三部分:
- 退出码 + coredump标志位 + 退出信号
- 高位字节表示退出码;
- 低位的一个字节又被分为两个部分,
- 第一个比特位表示coredump标志位
- 后七个比特位表示退出信号
3.退出码:程序正常退出时用到
- main函数的return返回时,return后的数字就是退出码
- exit和_exit中的参数就是退出码
4.coredump标志位,退出信号是程序异常退出时用到:
- coredump为0表示该进程在退出时没有内存镜像文件的产生(也就是说没有coredump产生),
- 为1则有镜像文件产生,当前程序退出异常
- 退出信号:进程异常退出时收到的几号信号
- 进程正常退出时coredump标志位和退出信号被置为全0;
退出码则是进程正常退出给的数值; - 异常退出coredump标志位和退出信号会有相应的数;
也就是说程序正常退出只有退出码起作用,异常退出则要看标志位和退出信号
waitpid 函数
函数原型
#include <sys/types.h>