Linux 下进程操作,----进程的创建与控制
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Linux 下进程操作,----进程的创建与控制相关的知识,希望对你有一定的参考价值。
---恢复内容开始---
进程是一个程序一次执行的过程,是操作系统动态执行的基本单元。
进程的概念主要有两点:第一,进程是一个实体。每个进程都有自己的虚拟地址空间,包括文本区、数据区、和堆栈区。文本区域存储处理器执行的代码;数据区存储变量和动态分配的内存;堆栈区存储着活动进程调用的指令和本地变量。第二,进程是一个“执行中的程序”,它和程序有本质区别。程序是静态的,它是一些保存在磁盘上的指令的有序集合;而进程是一个动态的概念,它是一个运行着的程序,包含了进程的动态创建、调度和消亡的过程,是 Linux 的基本调度单位。只有当处理器赋予程序生命时,它才能成为一个活动的实体,称之为进程。
内核的调度器复制在所有的进程间分配cpu执行时间,也称时间片,它轮流在每个进程分得的时间片用完后从进程那里抢回控制权。
进程的内存布局:
数据区:程序使用的静态变量。
堆:程序可从该区域动态分配额外内存。
栈:随函数调用、返回而增减的一片内存,用于为局部变量和函数调用链接信息分配存储空间。
进程标识:
操作系统OS 会为每个进程分配一个唯一的整型 ID,做为进程的标识号(pid)。进程 0 是调度进程,常被成为交换进程,它不执行任何程序,是内核的一部分,因此也被成为系统进程。进程除了自身的 ID 外,还有父进程 ID(ppid)。也就是说每个进程都必须有它的父进程,操作系统不会无缘无故产生一个新进程。所有进程的祖先进程是同一个进程,它叫做 init 进程,ID 为 1,init 进程是内核自举后的第一个启动的进程。init 进程负责引导系统、启动守护(后台)进程并且运行必要的程序。它不是系统进程,但它以系统的超级用户特权运行。
pid唯一的标识进程由以下两个函数获得:
pid_t getpid(void) //PID(本身)
pit_t getppid(void) //ppid(父进程)
首先可以通过ps命令来查询正在运行的进程;
如:ps -eo pid,comm,cmd 命令。
(-e 表示列出全部进程,-o pid,comm,cmd 表示我们需要 PID,COMMAND,CMD 信息)
进程的用户 ID 与组 ID(进程的运行身份)
进程在运行过程中,必须具有一类似于用户的身份,以便进行进程的权限控制,缺省情况下,哪个登录用户
运行程序,该程序进程就具有该用户的身份。
例如,假设当前登录用户为 gotter,他运行了 ls 程序,则 ls 在运行过程中就具有 gotter 的身份,该 ls 进程的用户 ID 和组 ID 分别为 gotter 和 gotter 所属的组。这类型的 ID 叫做进程的真实用户 ID 和真实组 ID。真实用户 ID 和真实组 ID 可以通过函数 getuid()和 getgid()获得。
与真实 ID 对应,进程还具有有效用户 ID 和有效组 ID 的属性,内核对进程的访问权限检查时,它检查的是进程的有效用户 ID 和有效组 ID,而不是真实用户 ID 和真实组 ID。缺省情况下,用户的(有效用户 ID 和有效组 ID)与(真实用户 ID 和真实组 ID)是相同的。有效用户 id 和有效组 id 通过函数 geteuid()和 getegid()获得的。
id 命令可以显示真实有效的用户 ID(UID) 和组 ID(GID)。UID 是对一个用户的单一身份标识。组 ID(GID)则对应多个 UID。
进程的状态
进程是程序的执行过程,根据它的生命周期可以划分成 3 种状态,如下图所示。
执行态:该进程正在运行,即进程正在占用 CPU。
就绪态:进程已经具备执行的一切条件,正在等待分配 CPU 的处理时间片。
等待态:进程不能使用 CPU,若等待事件发生(等待的资源分配到)则可将其唤醒。
进程相关的命令操作:
nice 命令: 按用户指定的优先级运行进程:
nice 命令用来设置优先级,优先级的数值为-20~19,其中数值越小优先级越高,数值越大优先级越低,-20 的优先级最高,19 的优先级最低。需要注意的是普通用户只能在 0~19 之间调整应用程序的优先权值,只有超级用户有权调整更高的优先权值(从-20~19)。
nice [-n <优先级>] [--help] [--version] [执行指令] 选项介绍:
-n <优先级> 指定优先级;
--help 帮助信息;
--version 版本信息;
查看进程详细信息
ps -l 打印结果:
F S UID PID PPID C PRI NI ADDR SZ WCHAN TTY TIME CMD
0 S 500 12866 12864 1 80 0 - 1681 - pts/0 00:00:00 bash
0 R 500 12884 12866 1 80 0 - 1619 - pts/0 00:00:00 ps
其中的几个重要信息有:
UID : 代表执行者的身份
PID : 代表这个进程的代号
PPID :代表这个进程是由哪个进程发展衍生而来的,亦即父进程的代号
PRI :代表这个进程可被执行的优先级,其值越小越早被执行
NI :代表这个进程的 nice 值
PRI 即进程的优先级,此值越小进程的优先级别越高。而 NI,也就是我们所要说的 nice 值(通过 nice 命令设置),其表示进程可被执行的优先级的修正数值。如前面所说,PRI 值越小越快被执行,那么加入 nice 值后,将会使得 PRI 变为:PRI(new)=PRI(old)+nice。所以,nice 命令设置的优先级不是程序最终的优先级,而只是优先级的修正数值。
renice 命令允许用户修改一个正在运行的进程的优先权。
用法:renice 优先级 [[-p] pid ] [[-g] pgrp ...] [[-u] 用户名 ...]
示例,修正 PID28390 进程的优先级为 10:
[[email protected] test]# renice 10 28390
28390: old priority 0, new priority 10
后台运行进程的查看与切换
n objs 命令查看后台运行的进程。
示例:
[[email protected] test]# ./a.out & [1] 28607 |
|
//提示该进程运行的 PID 号 |
[[email protected] test]# jobs [1]+ Running |
./a.out & |
//后台运行的进程 |
这里的 1 表示,后台程序的作业代号。
当程序在前台运行时,按下 ctrl+z 可以将程序放入后台,并且暂停运行。
fg 将后台中的命令调至前台继续运行。
bg 将一个在后台暂停的命令,变成继续执行。
杀死进程 kill 命令
杀死进程前需要获取到进程的 PID 号,可以使用 ps 命令获取,PID 号是进程的唯一标识符。示例:
[[email protected] test]# ps
PID TTY TIME CMD
27810 pts/0 00:00:00 su
27960 pts/0 00:00:00 bash
28709 pts/0 00:04:18 a.out
一般用法:
kill PID 号
kill -9 PID 号对于杀不死的进程可以加-9 参数强制杀死
查看进程状态:
ps -aux:查看各个进程状态,包括运行就绪等待等状态。
ps -aux| grep ‘aa‘:查找指定(aa)进程。
ps -ef:查看所有的进程的 pid,ppid 等信息。
ps -aux 看%cpu(cpu 使用量) %mem(内存使用量) ps 查看
进程创建与控制:
fork 函数pid_t fork(void);
在 linux 环境下,创建进程使用 fork 函数。
在 linux 中 fork 函数是非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
ork 函数被调用一次之后返回两次,有三种不同的返回值:
1、 在父进程中,fork 返回新创建的子进程的 PID 号
2、 在子进程中,fork 返回 0;
3、 如果出现错误,fork 返回一个负值。
fork 函数创建子进程的过程:
使用 fork 函数得到的子进程是父进程的一个复制品,它从父进程继承了进程的地址空间,包括进程上下文、进程堆栈、内存信息、打开的文件描述符、信号控制设定、进程优先级、进程组号、当前工作目录、根目录、资源限制、控制终端,而子进程所独有的只有它的进程号、资源使用和计时器等。通过这种复制方式创建出子进程后,原有进程和子进程都从函数 fork 返回,各自继续往下运行。也就是说,fork 不仅仅复制了进程的资源,更复制了原有进程的运行状态,所以复制出的新的进程(子进程)虽然什么都和父进程一样,但它从 fork 函数后面开始运行。但是原进程的 fork 返回值与子进程的 fork 返回值不同,在原进程中,fork 返回子进程的 pid,而在子进程中,fork 返回 0,如果 fork 返回负值,表示创建子进程失败。
vfork 函数 pid_t vfork(void);
vfork(建立一个新的进程)
vfork()会产生一个新的子进程.但是 vfork 创建的子进程与父进程共享数据段,而且由 vfork()创建的子进程将先于父进程运行。
vfork()用法与 fork()相似,但是也有区别,具体区别归结为以下几点:
11. fork():子进程拷贝父进程的数据段,代码段。 vfork():子进程与父进程共享数据段。
22. fork():父子进程的执行次序不确定。
vfork():保证子进程先运行,在调用 exec 或_exit 之前与父进程数据是共享的,在它调用 exec 或_exit 之后父进程才可能被调度运行。
33. vfork()保证子进程先运行,在她调用 exec 或_exit 之后父进程才可能被调度运行。如果在
调用这两个函数之前子进程依赖于父进程的进一步动作,则会导致死锁。
4当需要改变共享数据段中变量的值,则拷贝父进程。
**现在,很多嵌入式 Linux 系统的 fork 函数调用都采用 vfork 函数的实现方式。实际上 uClinux 所有的多进程管理都通过 vfork 来实现。
进程终止
进程的终止有5种方式:
main函数的自然返回return0;
调用exit函数
调用_exit函数
接收到某个信号。如ctrl+c SIGINT 、 ctrl+\\ SIGQUIT
调用abort函数,它产生SIGABRT信号,所以是上一种方式的特例。
前3种方式为正常的终止,后2种为非正常终止。但是无论哪种方式,进程终止时都将执行相同的关闭打开的文件,释放占用的内存等资源。只是后两种终止会导致程序有些代码不会正常的执行。
exit和_exit函数都是用来终止进程的。当程序执行到exit和_exit时,进程会无条件的停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止本程序的运行。但是它们是有区别的
exit 函数和_exit 函数的最大区别在于 exit 函数在退出之前会检查文件的打开情况,把文件缓冲区中的内容写回文件,就是图中的“清理 I/O 缓冲”。
由于 linux 的标准函数库中,有一种被称作“缓冲 I/O”操作,其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时,会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取;同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足一定的条件(如达到一定数量或遇到特定字符等),再将缓冲区中的内容一次性写入文件。这种技术大大增加了文件读写的速度,但也为编程带来了麻烦。比如有一些数据,认为已经写入文件,实际上因为没有满足特定的条件,它们还只是保存在缓冲区内,这时用_exit 函数直接将进程关闭,缓冲区中的数据就会丢失。因此,如想保证数据的完整性,建议使用 exit 函数。
n exit 和_exit 函数的原型:
#include <stdlib.h> //exit 的头件 #include <unistd.h> //_exit 的头文件
void exit(int status); void _exit(int status);
status 是一个整型的参数,可以利用这个参数传递进程结束时的状态。一般来说,0 表示正常结束;其他的数值表示出现了错误,进程非正常结束。
注意:’\\n’ 换行符就是缓冲区的结束符
孤儿进程
用 fork 函数启动一个子进程时,子进程就有了它自己的生命并将独立运行。
如果父进程先于子进程退出,则子进程成为孤儿进程,此时将自动被 PID 为 1 的进程(即 init)接管。孤儿进程退出后,它的清理工作有祖先进程 init 自动处理。但在 init 进程清理子进程之前,它一直消耗系统的资源,所以要尽量避免。
僵尸进程
如果子进程先退出,系统不会自动清理掉子进程的环境,而必须由父进程调用 wait 或 waitpid 函数来完成清理工作,如果父进程不做清理工作,则已经退出的子进程将成为僵尸进程(defunct),在系统中如果存在的僵尸(zombie)进程过多,将会影响系统的性能,所以必须对僵尸进程进行处理。
wait 和 waitpid 都将暂停父进程,等待一个子进程退出,并进行清理工作;
wait 函数随机地等待一个子进程退出,并返回该子进程的 pid;
waitpid 等待指定 pid 的子进程退出;如果为-1 表示等待所有子进程,同样返回该子进程的 pid。
status 参数是传出参数,它会将子进程的退出码保存到 status 指向的变量里
options 用于改变 waitpid 的行为,其中最常用的是 WNOHANG,它表示无论子进程是否退出都将立即返回,不会将调用者的执行挂起。
waitpid(pid, &n, 0);其中 n 是之前定义的整型变量。
通常用下面的两个宏来获取状态信息:
WIFEXITED(n) :如果子进程正常结束,它就取一个非 0 值。
WEXITSTATUS(n): 如果 WIFEXITED 非零,它返回子进程的退出码,即子进程的 exit()函数的参数的值,或者 return 返回的值。
查看系统全部环境变量
可以用 env 命令查看环境变量。
exec 函数族
exec 函数族的组成
extern char **environ; 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[]); int execve(const char * path, char *const argv[], char *const envp[]);
exec 函数族的作用是运行第一个参数指定的可执行程序。但其工作过程与 fork 完全不同,fork 是在复制一份原进程,而 exec 函数执行第一个参数指定的可执行程序之后,这个新程序运行起来后也是一个进程,而这个进程会覆盖原有进程空间,即原有进程的所有内容都被新运行起来的进程全部覆盖了,所以 exec 函数后面的所有代码都不再执行,但它之前的代码当然是可以被执行的。
path 是包括执行文件名的绝对路径,file 既可是绝对路径也可是可执行文件名
arg 是可执行文件的全部命令行参数,可以用多个,注意最后一个参数必须为 NULL。(比如:”ls”,”-l”,NULL)
argv 是一个字符串的数组 char *argv[]={“full path”,”param1”,”param2”,...NULL};
envp 指定新进程的环境变量 char *envp[]={“name1=val1”,”name2=val2”,...NULL};
exec 函数族的参数传递有两种方式:一种是逐个列举的方式,而另一种则是将所有参数整体构造指针数组传递。在这里是以函数名的第 5 位字母来区分的,字母为“l”(list)的表示逐个列举的方式,其语法为 char *arg;字母为“v”(vertor)的表示将所有命令行参数整体构造指针数组传递,其语法为 char *const argv[]。
以字母 p 结尾的函数通过搜索系统 PATH 这个环境变量来查找新程序的可执行文件的路径。如果可执行程序不在 PATH 定义的路径中,我们就需要把包括目录在内的使用绝对路径的文件名作为参数传递给函数。
对有参数 envp 的函数调用,其以字母 e 结尾,函数通过传递 envp 传递字符串数组作为新程序的环境变量。
新进程中的全局变量 environ 指针指向的环境变量数组将会被 envp 中的内容替代。
注意:对于有参数 envp 的函数,它会使用程序员自定义的环境变量,如果自定义的环境变量中包含了将要执行的可执行程序的路径,那么第一个参数中是不是我们就可以不用写全路径了呢?不是的,必须写全路径。因
为我们自定义的环境变量不是用来寻找这个可执行程序的,而是在这个可执行程序运行起来之后给新进程用的。
可以用env命令查看环境变量。
---恢复内容结束---
以上是关于Linux 下进程操作,----进程的创建与控制的主要内容,如果未能解决你的问题,请参考以下文章