一、进程
参考链接:https://doc.embedfire.com/linux/imx6/base/zh/latest/system_programing/process.html
一、进程组成
1、进程表
进程表是一个数据结构,它将当前加载在内存中的所有进程的有关信息保存到一个表中,其中包括进程的PID、进程的状态、命令字符串和其他一些ps命令输出的信息。操作系统通过进程的ID对它们进行管理,这些PID是进程表的索引。
2、进程ID
linux系统为每个进程分配一个唯一的数字编号,我们称为进程ID(PID)。
- 进程ID是一个16位的正整数。
- 默认取值范围从2到32768.(1为init进程的pid)
- 由linux在启动新进程时自动一次分配.
- 当PID的数值达到最大值时,系统将重新选择下一个未使用的数值。
3、父进程ID
- 任何进程(除init进程)都是由另一个进程启动,该进程被成为被启动进程的父进程,被启动的进程称为子进程。
- 父进程号无法在用户层修改。父进程的进程号(PID)即为子进程的父进程号(PPID).
- 用户可以通过调用getppid()函数来获取当前进程的父进程号。
- ps命令可以查看系统中的进程情况
4、父进程和子进程
每个进程都有一个父进程(除了系统中如“僵尸进程”这种特殊进程外),因此linux中的进程结构像一个树状结构,把init进程看作树根,它是所有进程的祖先进程。
pstree命令可以查看这个树状结构。
二、程序与进程
1、程序的概念
程序是为了完成特定任务而准备好的指令序列与数据的集合,这些指令和数据以“可执行映像”的格式保存在磁盘中。编译生成对应的可执行文件就是可执行程序。
2、进程的概念
进程是程序执行的具体实例
- 程序在执行的过程中,它享有系统的资源,至少包括进程的运行环境、CPU、外设、内存、进程ID等资源和信息。
- 同样的一个程序,可以实例化为多个进程。
- 程序不能单独执行,只有将程序加载到内存中,系统为它分配资源后才能执行,这种执行的程序称为进程。
- 也就是说进程是系统进行资源分配和调度的一个独立单位。
- 每个进程都有自己单独的地址空间。
3、程序变成进程
在linux系统中,程序只是个静态的文件,而进程是一个动态的实体。程序是如何变成一个进程的呢?
程序转化为进程的过程主要包含以下3个步骤:
- 查找命令对应程序文件的位置。
- 使用fork()函数为其启动一个新进程。
- 在新进程中调用exec族函数装载程序文件,并执行程序文件中的main函数。
4、总结
- 程序只是一系列指令序列与数据的集合。
- 进程是程序在某个数据集上的执行过程,它是一个动态运行的实体,有自己的生命周期。
- 进程和程序并不是一一对应的,一个程序执行在不同的数据集上运行就会成为不同的进程。
- 可以用进程控制块来唯一表示系统的每个进程。
- 进程具有并发性而程序没有。
- 进程是竞争计算机资源的基本单位,而程序不知。
三、进程状态
1、查看进程状态
状态 |
说明 |
---|---|
R |
运行状态。严格来说,应该是“可运行状态”,即表示进程在运行队列中,处于正在执行或即将运行状态,只有在该状态的进程才可能在 CPU 上运行,而同一时刻可能有多个进程处于可运行状态。 |
S |
可中断的睡眠状态。处于这个状态的进程因为等待某种事件的发生而被挂起,比如进程在等待信号。 |
D |
不可中断的睡眠状态。通常是在等待输入或输出(I/O)完成,处于这种状态的进程不能响应异步信号。 |
T |
停止状态。通常是被shell的工作信号控制,或因为它被追踪,进程正处于调试器的控制之下。 |
Z |
退出状态。进程成为僵尸进程。 |
X |
退出状态。进程即将被回收。 |
s |
进程是会话其首进程。 |
l |
进程是多线程的。 |
进程属于前台进程组。 |
|
< |
高优先级任务。 |
3、进程状态转换
- 一个进程的开始都是父进程调用fork()开始的,系统一上电,init进程开始工作,init进程启动其他新的进程。
- 一个进程被启动时,都是处于可运行状态(并为占用CPU),处于该状态的进程正在进程等待队列中排队,这种状态成为就绪态。
- 就绪态的进程获取CPU资源后,进程就转换为运行态,但是每个进程都有分配时间片,时间片耗光的时候,如果进程还没有结束运行,就会被系统重新放入等待队列中等待,此时进程又转变为就绪态,等待下一次进程的调度。另外,正在运行态的进程即使时间片没有耗光,也可以被更高优先级的进程抢占,被迫重新回到等待队列中。
- 运行态进程可能会等待某些时间、信号、或者资源进入”可中断睡眠态“,比如进程读取一个管道文件数据而管道为控,或者进程要获得一个锁资源而当前锁不可获取,甚至是进程自己调用sleep()来强制自己进入睡眠。”可中断睡眠态“就是可以被中断的,能响应信号的,在特定条件发生后,进程状态就会转换为”就绪态“。
- 运行态进程还可能会进入”不可中断睡眠态“,这种状态下,进程不能响应信号 ,但是这种状态非常短暂,一般处于这种状态的进程都是在等待输入或输出(io)完成,在等待完成后自动进入”就绪态“。
- 当进程收到SIGSTOP或者SIGTSTP信号,进程状态会被置为”暂停态“,该状态下的进程不再参与调度,但系统资源不会被释放,直到收到SIGCONT信号后被重新置为就绪态。
- 进程完成任务后会退出,那么此时进程状态变为退出状态。
- 当父进程去处理僵尸进程的时候,会将这个僵尸进程的状态设置为EXIT_DEAD,即死亡态(推退出态),这样子系统才能去回收僵尸进程的内存空间。
四、启动新进程
1、system函数启动进程
此方法相对简单,但效率低下,且具有不容忽视的安全问题。
(1)头文件
#include<stdlib.h>
(2)函数原型
int system(const char *command)
(3)返回值
- 失败返回-1;
- 成功则返回执行命令的状态
(4)例程
#include<stdlib.h>
#include<stdio.h>
int main()
{
int ret;
printf("This is a system demo!\n\n");
ret = system("ls -l");
printf("Done!\n\n");
return ret;
}
执行结果:
从程序的执行结果来看,只有当system()函数执行完毕后,才会输出Done,这是因为程序是从上往下执行的,无法直接返回结果。它必须等待由system()函数启动的进程结束之后才能继续,因此我们不能立即执行其他任务。
当然,也可以让”ls -l“命令在后台运行,只需在命令结束位置加上”&“即可,具体命令如下:
ls -l &
在system()中使用这个命令,那么这个命令是在后台运行的,system()函数将在shell命令结束后立即返回。修改system.c源码:
#include<stdlib.h>
#include<stdio.h>
int main()
{
pid_t ret;
printf("This is a system demo!\n\n");
ret = system("ls -l &"); //后台执行,system()立即返回
printf("Done!\n\n");
return ret;
}
有执行结果可知,ls -l在后台执行后,在指令执行完毕还未打印出来之前system()就返回了,先打印出了Done.
2、fork()函数启动进程
提供了更好的弹性、效率和安全性。
(1)头文件
#include<sys/types.h>
#include<unistd.h>
(2)函数原型
pid_t fork(void);
(3)返回值
- 成功则返回0,.
- 失败返回-1。
- 返回值大于0代表父进程
在父进程中的fork()调用后返回的是新的子进程的PID.新进程将继续执行,像原进程一样,不同之处在于,子进程中的fork()函数调用后返回的是0,父子进程可以通过返回的值来判断究竟谁是父进程,谁是子进程。
fork()函数的本质是将父进程的内容复制一份,因此子进程基本上是父进程的一个复制品,但子进程和父进程有不一样的地方,区别与联系如下:
(4)子进程与父进程一致的地方:
- 进程的地址空间
- 进程上下文、代码段
- 进程堆空间、栈空间、内存信息。
- 进程的环境变量。
- 标准IO的缓冲区。
- 打开的文件描述符
- 信号响应函数
- 当前工作路径。
(5)子进程独有的内容
- 进程号PID。
- 记录锁。父进程对某文件加了把锁,子进程不会继承这把锁。
- 挂起的信号。这些信号是已经响应但尚未处理的信号,子进程也不会继承这些信号。
因为子进程几乎是父进程的完全复制,所以父子进程会运行同一个程序,但这种复制对于资源和时间的消耗就很大,当调用fork()函数时,内核会复制父进程的整个地址空间给子进程,这中型为是非常耗时的,因为需要做以下事情:
- 为子进程的页表分配页面
- 为子进程的页分配页面。
- 初始化子进程的页表。
- 把父进程的页复制到子进程相应的页中。
在调用fork()函数后立即执行exec(),地址空间就不会被复制了,这样fork()的实际开销就是复制父进程的页表以及给子进程创建一个进程描述符。
(6)例程
#include<stdlib.h> #include<stdio.h> #include<sys/types.h> #include<unistd.h> int main() { pid_t ret; printf("This is a fork demo!\n\n"); ret = fork(); if(ret == -1) { printf("create child process failed!\ns"); }
/*返回值为0,代表子进程*/ else if(ret == 0) { printf("sucessed to create process,ret=%d,pid=%d\n",ret,getpid()); }
/*返回值大于0代表父进程*/ else { printf("parent process ret=%d,pid=%d\n",ret,getpid()); } printf("Done!\n\n"); return ret; }
执行结果:
在这个实验现象中,父进程的返回值就是子进程的PID,而子进程的返回值则是0。 而且子进程并不会再次执行fork()函数之前的内容,而fork()函数之后的内容在父进程和子进程都会执行一遍。
3、exec系列函数进程实验
使用fork函数启动一个子进程并没有太大作用,因为子进程和父进程都是一样的,子进程能干的活父进程也一样能干,如果让子进程做不一样的事情,则需要使用exec系列函数用于替换进程的执行程序。它可以根据指定的文件名或目录找到可执行文件,并用它来取代原调用进程的数据段、代码段和堆栈段。
(1)头文件
#include<unistd.h>
(2)函数原型
int execl(const char *path,const char *argv,....);
execl()函数用于执行参数path字符串所代表的文件路径(必须指定路径), 接下来是一系列可变参数,它们代表执行该文件时传递过去的 argv[0]、argv[1]… argv[n]
, 最后一个参数必须用空指针NULL作为结束的标志。
(3)返回值
- 错误则返回-1,
(4)例程
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<unistd.h>
int main()
{
int ret;
printf("This is a execl demo!\n\n");
ret = execl("/bin/ls","ls","-l",NULL);
if(ret < 0)
{
printf("execl failed!\ns");
}
printf("Done!\n\n");
return ret;
}
执行结果:
执行结果没有打印出Done,因为excel替换了当前进程去执行ls -l.execl函数执行后,当前进程将不会再继续执行,所以程序的Done不被输出。一般情况下,exec系列函数是不会返回的,除非发生错误。出现错误时,exec系列函数将返回-1,并且会设置错误变量errno.
因此我们可以通过调用fork()复制启动一个子进程,并且在子进程中调用exec系列函数替换子进程, 这样把fork()和exec系列函数结合在一起使用就是创建一个新进程所需要的一切了.
(5)exec族的其他函数
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[])
五、终止进程
1、正常终止
- 从main函数返回
- 调用exit()函数终止。
- 调用_exit()函数终止。
2、异常终止
- 调用abort()函数异常终止。
- 由系统信号终止。
在linux系统中,exit()函数定义在stdlib.h中,而_exit()定义在unistd.h中,exit()和_exit()函数都是用来终止进程的,当程序执行到exit()或_exit()函数时,进程会无条件地停止剩下的所有操作,清除包括PCB在内的各种数据结构,并终止当前进程的运行。不过这两个函数还是有区别的,具体下图所示。
- _exit函数:终止进程的时候会清除整个进程使用的内存空间,并销毁它在内核中的各种数据结构
- exit函数:不仅要实现_exit函数的功能,还要再调用之前检查文件的打开情况,把文件缓冲区的内容写回文件,这就是“清除IO缓存”
“缓冲 I/O(buffered I/O)”操作:
其特征就是对应每一个打开的文件,在内存中都有一片缓冲区。每次读文件时, 会连续读出若干条记录,这样在下次读文件时就可以直接从内存的缓冲区中读取; 同样,每次写文件的时候,也仅仅是写入内存中的缓冲区,等满足了一定的条件(如达到一定数量或遇到特定字符等), 再将缓冲区中的内容一次性写入文件。
这种技术大大增加了文件读写的速度,但也为编程带来了一些麻烦。 比如有些数据,程序认为已经被写入文件中,实际上因为没有满足特定的条件,它们还只是被保存在缓冲区内, 这时用_exit()函数直接将进程关闭,缓冲区中的数据就会丢失。 因此,若想保证数据的完整性,就一定要使用exit()函数。
不管是那种退出方式,系统最终都会执行内核中的同一代码,这段代码用来关闭进程所用已打开的文件描述符, 释放它所占用的内存和其他资源。
3、_exit()与exit()函数的使用方法:
(1)头文件
#include <unistd.h>
#include <stdlib.h>
(2)函数原型
void _exit(int status);
void exit(int status);
这两个函数都会传入一个参数status,这个参数表示的是进程终止时的状态码,0表示正常终止, 其他非0值表示异常终止,一般都可以使用-1或者1表示,标准C里有EXIT_SUCCESS和EXIT_FAILURE两个宏, 表示正常与异常终止。
六、等待进程
在Linux中,当我们使用fork()函数启动一个子进程时,子进程就有了它自己的生命周期并将独立运行, 在某些时候,可能父进程希望知道一个子进程何时结束,或者想要知道子进程结束的状态, 甚至是等待着子进程结束,那么我们可以通过在父进程中调用wait()或者waitpid()函数让父进程等待子进程的结束。
从前面的文章我们也了解到,当一个进程调用了exit()之后,该进程并不会立刻完全消失, 而是变成了一个僵尸进程。僵尸进程是一种非常特殊的进程,它已经放弃了几乎所有的内存空间, 没有任何可执行代码,也不能被调度,仅仅在进程列表中保留一个位置, 记载该进程的退出状态等信息供其他进程收集,除此之外,僵尸进程不再占有任何内存空间。 那么无论如何,父进程都要回收这个僵尸进程,因此调用wait()或者waitpid()函数其实就是将这些僵尸进程回收, 释放僵尸进程占有的内存空间,并且了解一下进程终止的状态信息。
1、wait()函数
(1)头文件
#include <sys/types.h>
#include <sys/wait.h>
(2)函数原型
pid_t wait(int *wstatus);
wait()函数在被调用的时候会做以下事情:
- 系统将暂停父进程的执行,直到有信号来到或子进程结束
- 如果在调用wait()函数时子进程已经结束,则会立即返回子进程结束状态值。
- 子进程的结束状态信息会由参数wstatus返回,与此同时该函数会返回子进程的PID, 它通常是已经结束运行的子进程的PID。状态信息允许父进程了解子进程的退出状态, 如果不在意子进程的结束状态信息,则参数wstatus可以设成NULL。
(3)注意点
-
wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1, 正常情况下wait()的返回值为子进程的PID。
-
参数wstatus用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针, 但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉, (事实上绝大多数情况下,我们都会这样做),我们就可以设定这个参数为NULL。
当然,Linux系统提供了关于等待子进程退出状态的一些宏定义, 我们可以使用这些宏定义来直接判断子进程退出的状态:
-
WIFEXITED(status) :如果子进程正常结束,返回一个非零值
-
WEXITSTATUS(status): 如果WIFEXITED非零,返回子进程退出码
-
WIFSIGNALED(status) :子进程因为捕获信号而终止,返回非零值
-
WTERMSIG(status) :如果WIFSIGNALED非零,返回信号代码
-
WIFSTOPPED(status): 如果子进程被暂停,返回一个非零值
-
WSTOPSIG(status): 如果WIFSTOPPED非零,返回一个信号代码
(4)实验分析
wait()函数使用实例如下:
#include<stdlib.h>
#include<stdio.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<unistd.h>
void main()
{
pid_t pid,child_pid;
int status;
pid = fork();
if(pid < 0)
{
printf("error fork\n");
}
/*子进程*/
else if(pid == 0)
{
printf("I am a child process,my pid is %d!\n",getpid());
sleep(3);
printf("I am about to quilt the process!\n");
exit(0); //退出子进程
}
/*父进程*/
else
{
child_pid = wait(&status);/*先执行子进程,子进程退出后,wait等待子进程的结束*/
/*发现子进程退出,打印出相应情况*/
if(child_pid == pid)
{
printf("Get exit child process id:%d\n",child_pid);
printf("Get child exit status:%d\n",status);
}
else
{
printf("some error occured.\n");
}
exit(0); //退出父进程
}
}
执行结果:
-
首先调用fork()函数启动一个子进程。
-
如果fork()函数返回的值pid为0,则表示此时运行的是子进程,那么就让子进程输出一段信息,并且休眠3秒。
-
休眠结束后调用exit()函数退出,退出状态为0,表示子进程正常退出。
-
如果fork()函数返回的值pid不为0,则表示此时运行的是父进程, 那么在父进程中调用wait(&status)函数等待子进程的退出,子进程的退出状态将保存在status变量中。
-
若发现子进程退出(通过wait()函数返回的子进程pid判断),则打印出相应信息,如子进程的pid与status。
2、waitpid()
waitpid()函数的作用和wait()函数一样,但它并不一定要等待第一个终止的子进程, 它还有其他选项,比如指定等待某个pid的子进程、提供一个非阻塞版本的wait()功能等。 实际上wait()函数只是 waitpid() 函数的一个特例,在 Linux内部实现 wait函数时直接调用的就是 waitpid 函数。
(1)函数原型
pid_t waitpid(pid_t pid, int *wstatus, int options);
waitpid()函数的参数介绍:
-
pid:参数pid为要等待的子进程ID,其具体含义如下:
-
pid < -1:等待进程组号为pid绝对值的任何子进程。
-
pid = -1:等待任何子进程,此时的waitpid()函数就等同于wait()函数。
-
pid =0:等待进程组号与目前进程相同的任何子进程, 即等待任何与调用waitpid()函数的进程在同一个进程组的进程。
-
pid > 0:等待指定进程号为pid的子进程。
-
-
wstatus:与wait()函数一样。
-
options:参数options提供了一些另外的选项来控制waitpid()函数的行为。 如果不想使用这些选项,则可以把这个参数设为0。
-
WNOHANG:如果pid指定的子进程没有终止运行,则waitpid()函数立即返回0, 而不是阻塞在这个函数上等待;如果子进程已经终止运行,则立即返回该子进程的进程号与状态信息。
-
WUNTRACED:如果子进程进入了暂停状态(可能子进程正处于被追踪等情况),则马上返回。
-
WCONTINUED:如果子进程恢复通过SIGCONT信号运行,也会立即返回(这个不常用,了解一下即可)。
-
很显然,当waitpid()函数的参数为(子进程pid, status,0)时,waitpid()函数就完全退化成了wait()函数。
jobs:查看有哪些后台进程组 fg+job id可以把后台进程组切换为前台进程组