程序与进程:
程序是已经写好的并经过编译后得到的二进制文件;进程是运行起来的二进制文件。所以在这里,我们可以浅显的将程序理解为死的,而进程是活的。比较形象的类比是:
程序是一个剧本,进程是一场演出。一方面:剧本只占用一张纸的资源,而演出需要耗费人力、物物力、时间、场地等等资源。程序与进程也是如此,程序只占用一部分磁盘空间,而进程则会消耗CPU、内存等各种资源。另一方面:我们可以根据剧本在不同的时间、地点进行该演出。而程序与进程也同样,我们可以在不同的终端运行同一个程序,此时该程序就存在两个进程。
有了上面基础的认识,我们就要开始正式理解进程了:
基本概念
进程是正在执行程序的实例,是操作系统进行资源分配和调度的基本单位。在内核看来,进程是一个个实体,内核需要在它们之间共享各种计算机资源。
即使只有一个 CPU 也支持并发。例如,当一个程序在读取磁盘文件,硬盘的读写速度是很慢的,如果CPU一直等到硬盘返回数据,CPU的利用率就会很低。所以,当进程从硬盘读取数据时,CPU不需要阻塞等待数据的返回,而去执行其他进程。当硬盘数据返回时,给CPU发送中断,CPU就可以回来继续执行了。
CPU 会在进程间快速切换,使每个程序运行几十或者几百毫秒。然而,在任意一个瞬间,CPU 只能运行一个进程,如果把时间定位为 1 秒内的话,它可能运行多个进程。这样就会让我们产生并行的错觉。
CPU可以通过交替执行程序管理多个进程,CPU在多个进程间快速切换,每个进程各运行几十或几百毫秒,然而,在任意一个瞬间,CPU 只能运行一个进程,如果把时间定位为 1 秒内的话,它可能运行多个进程。这样就会让我们产生并行的错觉,实际上是并发。
进程状态
当一个进程开始运行时,它可能会经历以下几个状态:
运行态:指的是进程实际占用CPU的时间片运行时
就绪态:指的是可运行,但因为其它进程正在运行而处于就绪状态
阻塞态:也成为等待态,该状态的进程正在等待某一事件发生(如:等待输入/输出操作的完成)而暂时停止运行,这时即使给该进程CPU控制权,它也无法运行。
状态切换:
1.运行态到阻塞态:当进程遇到某个事件需要等待时会进入阻塞态
2.阻塞态到就绪态:当进程要等待的事件完成时会从阻塞态变为就绪态
3.就绪态到运行态:处于就绪态的进程被操作系统的进程调度程序选中后,就分配CPU控制权开始运行。
4.运行态到就绪态:该程序运行过程中,分配给它的时间片用完后,操作系统会将其变为就绪态,接着从就绪态的进程中选择一个运行。(按照进程队列选择)
程序调度:指的是:决定哪个进程优先被运行和运行多久。目前已经设计出了很多的算法来尝试平衡系统整体效率和各个流程之间的竞争需求。
进程控制块PCB
每个进程在内核中都有一个进程控制块(PCB)来维护进程相关的信息,Linux 内核的进程控制块是 task_struct 结构体。 其内部成员有很多,以下是重点部分:
- 进程 id,每个进程有唯一的 id,用 pid_t 类型表示,其实就是一个非负整数
- 进程的状态,有就绪、运行、挂起状态
- 描述虚拟地址空间的信息
- 文件描述符表,包含很多指向 file 结构体的指针
- 进程切换时需要保存和恢复的一些 CPU 寄存器
- 描述控制终端的信息
- 当前工作目录
- umask 掩码
- 和信号相关的信息
- 用户 id 和组 id
- 会话(Session)和进程组
- 进程可以使用的资源上限(Resource Limit)
这里我们最经常使用的就是:进程id、文件描述符fd了。关于进程控制块PCB我们在前文也提到了。(【Linux】文件IO--open/close/文件描述符(详)_linux open close-CSDN博客)这里重新巩固一下:(强调:这是虚拟内存分布模型,不是真正的存储位置)
PCB进程控制块是一个结构体,里面包含进程的一些基本信息,每一个进程都会维护一个自己的PCB进程控制块。这是我们需要牢记的。
而对于整个系统而言,管理多个PCB的方式其实是管理一个PCB链表:(PCB存在于磁盘在内存中的映射部分,而程序文件切切实实的存储在磁盘中。)
进程创建
创建进程的函数是fork。第一步:认识函数原型(man 2 fork)
函数原型:pid_t fork(void) -create a child process/创建一个子进程
包含头文件:<sys/types.h>、<unistd.h>
函数返回值:pid_t类型,代表的是一个子进程id。如果返回-1代表创建失败。
对于该函数有一个需要注意的地方就是,创建完子进程后,在子进程该函数调用处的返回值是0。
也就是,区分父子进程的关键就在于在该进程中返回值fpid的值是否大于0,如果大于0那么就是父进程,fpid代表子进程的进程id,如果等于0,则代表该进程是子进程。
示例:
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<errno.h>
int main(){
printf("father only\n");
pid_t fpid = fork();
if(fpid < 0){perror("fork err");exit(-1);}
if(fpid == 0){
printf("i am child process\n");
}
if(fpid > 0){
printf("i am father process\n");
}
printf("father and child \n");
return 0;
}
分析过程:进入main函数后,第一步打印father only,然后创建子进程,子进程创建后会将整个进程的资源拷贝一份独立存在。此时,对于该程序已经有了两个进程,对于父进程来讲fpid是子进程的id>0,对于子进程来讲,fpid=0。所以根据返回值fpid,我们的两份进程分别进入不同的if入口,父进程打印i am father process,子进程则打印i am child process。然后父子进程都执行到最后一句,则会分别打印出father and child。但是父子进程谁运行的快,谁运行的慢,我们并不知道,所以从fork()之后的输出语句的输出顺序并不唯一,但相对顺序一致,也就是两句father and child一定分别在i am之后。下面看一下运行结果。
运行结果:
多次运行,我们会发现运行的结果并不唯一,但输出的语句的条数以及相对的输出顺序都保持一致。
进程就这么被创建出来了,有什么问题吗?有的,我们将从下面几个方面探讨:
资源问题
上面我们提到了一句:拷贝资源、独立存在
当子进程进行创建时,父进程的所有资源都被子进程拷贝了一份并维护在子进程自己的PCB中,这就是我们上文提到的:每一个进程都有自己的进程控制块PCB。通过父进程创建的子进程也是一样的。那么我们如何验证资源的独立性呢?
int a = 10;
pid_t fpid = fork();
if (fpid < 0)
{
perror("fork err");
exit(-1);
}
if (fpid == 0)
{
a+=10;
printf("i am child process:%d \n",a);
}
if (fpid > 0)
{
a+=5;
printf("i am father process:%d\n",a);
}
printf("%d\n",a);
while(1);
我们可以发现,在父进程中,a+=5,a被修改到了15,而子进程中a+=10,a被修改到了20,最后自己的进程运行到最后一句printf时,都将自己的a打印了出来。仍然是15和20。我们假设资源不是独立的,那么不管谁先运行完,那么随后的都将是前者修改的基础上进行的修改。所以至少会出现35的值。但结果并没有,这就是因为他们修改的都是属于自己的变量a。这就是拷贝资源、独立存在。
但是,我们也会经常碰见这么一个问题:父子进程间什么内容是共享的?
首先,我们直到子进程创建时会按照父进程的资源拷贝一份,进程的资源由PCB进行维护,而PCB中比较重要的一个除了进程id-pid外,就是文件描述符表:子进程将父进程的文件描述符表拷贝了下来,然后父进程中打开的文件的文件描述符fd(假设fd=3),子进程中也会有一个fd=3。这时你会疑惑,两份进程的资源不是独立的吗,是的,是独立的,pid1中有一个fd=3,子进程pid2中自然也会有一个fd=3,并且这个文件描述符指向的内容也被拷贝了下来,也就是fd是数组索引,arr[fd]是fd指向的值,这个值就是一个指针,一个能找到文件位置的指针作用的值,此时它们指向同一个文件条目,而这个文件条目存储于进程之外,例如:我们父进程操作fd时,向文件中写入了一段内容,那么当子进程操作同一个fd时,想在文件中读取文件,必须要将文件的偏移量进行偏移文件首部才行。
对于文件条目,也就是目录项,他是管理文件的一个结构体结点,采用引用计数的方式存在,当两个进程对该结点同时关闭时,才会被删除,所以两个文件相当于共享同一个文件描述符,也就共享文件偏移量。那如果是两个独立的程序分别打开同一个文件,那么会生成两个目录项,两个目录项分别指向同一个文件,所以文件偏移量不共享。这样说大家是不是可以理解了。
进程终止
下面我们来看另一块内容:
首先,我们直到,进程一旦创建,两个进程执行的顺序将无法预测。所以大概率会有一个先执行完,有一个后执行完。下面我们对两个情况分别进行探讨:(1)父进程先结束(2)子进程先结束
子进程先结束
子进程先结束,那就正常退出程序,没毛病。
父进程先结束
写一个程序,让子进程分支sleep(1)休眠一秒,保证父进程执行完毕。
int main(int argc, char* argv[])
{
pid_t fpid=fork();
if(fpid<0)exit(1);//创建子进程失败
if(fpid==0){//子进程
sleep(1);//休眠
printf("child:%u\n", getpid());
while(1);
printf("child is end\n");//预期-子进程执行不到该句
}
if(fpid>0){//父进程
printf("father:%u\n",getpid());
}
return 0;
}
编译执行程序:
左侧终端观察,父进程执行完毕,return 0,程序结束(第一种结束进程的方式:return);从右侧终端观察,子进程仍在执行。起始从父进程结束的那一刻起,子进程就成为了孤儿进程。但是由于操作系统有一个进程id为1的init进程,专门“收养”这些孤儿进程,我们没法直白的观察到现象来证明他成为了孤儿进程。知道有这个概念就行了。此时我们不可能让这么一个进程来运行着占用资源,我们需要杀死进程,来回收资源。
第二种结束进程的方式:
进程状态
除此以外,还有其他一些进程状态:
static const char * const task_state_array[] = {
"R (running)", /* 0 */---运行
"S (sleeping)", /* 1 */---浅度睡眠,随时被唤醒,被杀掉
"D (disk sleep)", /* 2 */---深度睡眠,不会被杀掉,只有自己主动唤醒才能恢复
"T (stopped)", /* 4 */---暂停
"t (tracing stop)", /* 8 */---进程被调试的时候,遇到断点所处的状态
"X (dead)", /* 16 */---死亡
"Z (zombie)", /* 32 */---僵尸
};
1.运行状态(R,running):并不意味着进程一定在运行中,它表示的是,进程要么在运行中,要么在运行队列中等待运行。
2.睡眠状态(S,sleeping):意味着进程在等待事件完成,这里的睡眠有时也叫可中断睡眠,进程执行sleep()时就会进入该状态
3.磁盘休眠状态(D,Disk sleep):又叫做不可中断睡眠,在这个状态下的进程通常会等待IO的结束。
4. 暂停状态(T,stopped):可以发送SIGSTOP信号给进程来停止进程,这个被暂停的基础南横可以通过发送SIGCONT信号让进程继续运行。
5.死亡状态(X,dead):这个状态是要给返回状态,你不会在人物列表看到这个状态的
6.僵尸状态(Z,zombie):一个比较特殊的状态,当进程退出并且,父进程没有读取到子进程推出的返回代码时就会产生僵尸进程。
补充:
前台进程:可以被ctrl+c杀掉的进程,命令行在这个终端可以起作用,STAT列的S+、R+后面的+号就代表前台进程的意思
后台进程:无法被ctrl+c杀死的进程,命令行在这个终端也能起作用,STAT列没有+号就表示后台进程的意思
僵尸进程
僵尸进程
僵尸进程(Zombie Process)是指一个已经终止执行的进程,但仍然在系统的进程表中占有一个条目的状态。这种状态发生是因为进程的父进程尚未使用 wait
系统调用收集其终止状态,导致子进程的信息仍未被清理。
产生的原因:子进程完成执行后,向其父进程发送 SIGCHLD 信号,通知它有子进程结束,但如果父进程没有响应(未调用 wait
),子进程就会变成僵尸进程。
- 状态:僵尸进程的状态通常被标记为
Z
,表示它已终止但仍存在于进程表中。- 资源释放:僵尸进程不会消耗系统资源,如 CPU 时间或内存,但仍占用进程表的一个条目。
- 父进程:通常,保持僵尸状态的进程是其父进程需要调用有效的回收操作(如
wait
)才能清理它。
僵尸进程不会自行消失,但处理不当可能导致潜在的问题,因为过多的僵尸进程会耗尽进程表中的条目,造成资源紧张。(有时这种情况也称为内存泄漏)。
内存回收
- 在程序执行过程中,可能会分配内存来存储数据和指令。完成任务后,这些内存变得不再需要。内存回收的任务是释放这些不再被使用的资源,以便其他程序可以使用。
- 常见的内存回收策略包括垃圾回收(Garbage Collection)和手动内存管理。
进程回收
- 操作系统在一个进程终止后,会对资源进行清理和回收,确保系统资源不会浪费。例如,关闭打开的文件、释放所占用的内存空间等。
一个进程在终止时会关闭所有文件描述符,释放在用户空间分配的内存,但它的 PCB 还保留着,内核在其中保存了一些信息:如果是正常终止则保存着退出状态,如果是异常终止则保存着导致该进程终止的信号是哪个。这个进程的父进程可以调用 wait 或 waitpid 获取这些信息,然后彻底清除掉这个进程。我们知道一个进程的退出状态可以在 Shell 中用特殊变量$?查看,因为 Shell 是它的父进程,当它终止时 Shell 调用 wait 或 waitpid 得到它的退出状态同时彻底清除掉这个进程。
进程等待
为了避免孤儿进程和僵尸进程的产生,我们通常会等待子进程结束然后父进程再进行操作。这里有两个函数,先来看一下函数原型:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *wstatus);
pid_t waitpid(pid_t pid, int *wstatus, int options);
wait函数
函数作用描述:
父进程调用wait函数可以回收子进程的终止信息。该函数有三个功能:
1.阻塞等待 子进程的退出。2.回收 子进程残留资源。 3.获取子进程结束状态(退出原因)。
函数的参数:
status为传出参数,用来保存进程的退出状态,相当于一个额外的返回值。
函数返回值:如果成功将返回清理掉的子进程的id;如果失败返回-1(例如:没有子进程时)
当进程终止时,操作系统的隐式回收机制会:关闭所有文件描述符;释放用户空间、分配的内存等,但内核的PCB仍然存在,其中保存该进程的退出状态(正常终止->退出值; 异常终止->终止信号)。
#include<sys/wait.h>
int pid;
for (int i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (pid > 0) { // 只有父进程进入判断 pid就是子进程id
int wpid = wait(NULL); // 回收一个进程,剩余的四个进程会变成僵尸进程
printf("wpid = %d\n", wpid);
while(1);
} else {
sleep(10);
return 0;
}
可使用 wait 函数传出参数 status 来保存进程的退出状态。借助宏函数来进一步判断进程终止的具体原因。宏函数可分为如下三组:
组1.正常终止判断
WIFEXITED(status) //为真 → 进程正常结束
WEXITSTATUS(status) //如上宏为真,获取进程退出状态 (exit的参数)
组2.异常终止判断
WIFSIGNALED(status) //为真 → 进程异常终止
WTERMSIG(status) //如上宏为真,得使进程终止的那个信号的编号。
*组3.进程暂停判断
WIFSTOPPED(status) //为真 → 进程处于暂停状态
WSTOPSIG(status) //如上宏为真,取得使进程暂停的那个信号的编号。
WIFCONTINUED(status) //如上宏为真,进程暂停后已经继续运行
代码示例
正常结束:
int pid;
for (int i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (pid > 0) {
int status;
int wpid = wait(&status);
printf("wpid = %d\n", wpid);
if (WIFEXITED(status)) {
printf("status: %d\n", WEXITSTATUS(status));
}
// if (WIFSIGNALED(status)) {
// printf("signal: %d\n", WTERMSIG(status));
// }
while(1);
} else {
// while(1);
sleep(10);
return 5;
}
异常结束:
int pid;
for (int i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
}
if (pid > 0) {
int status;
int wpid = wait(&status);
printf("wpid = %d\n", wpid);
if (WIFSIGNALED(status)) {
printf("signal: %d\n", WTERMSIG(status));
}
while(1);
} else {
while(1);
sleep(10);
return 5;
}
waitpid函数
函数描述:与wait作用类似,但可以指定进程id为pid的进程进行清理,可以不阻塞
返回值:成功返回清理掉的子进程的id,失败返回-1(无子进程);如果参数三设置为WNOHANG,并且子进程正在运行,那么返回0。
参数:
1.pid:回收指定id的子进程,如果设置为-1,回收任意子进程,将waitpid函数当作wait函数使用。如果设置为0,回收当前调用进程的一个组的任一子进程、如果设置进程id<-1,回收指定进程组内的任意子进程。(后两种暂时接触不到)
2.wstatus:与wait一样。传出参数:退出状态。
3.options:如果想阻塞等待进程结束,参数传0;如果想立刻返回,不阻塞,传WNOHANG(wait no hang),如果进程结束,也能正常回收。
int pid;
int wid;
for (int i = 0; i < 5; i++) {
pid = fork();
if (pid == 0) {
break;
}
if (i == 2) { // 只能放在这里,如果放在上面,可能是子进程进来,wid可能为0
wid = pid;
printf("i = 2 pid = %d\n", wid);
}
}
if (pid > 0) {
int wpid = waitpid(wid, NULL, 0);
printf("wpid = %d\n", wpid);
while(1);
}
else {
sleep(5);
return 5;
}
exec函数族
fork创建子进程后执行的是和父进程相同的程序,执行不同的分支(同一套剧本的不同走向或同一套剧本的同一走向),子进程往往要调用一种exec函数去执行另一个程序。当进程调用一种exec函数时,通过调用,进程能以全新的程序来替换掉当前运行的程序。将当前进程的代码段和数据段替换为所要加载的程序的代码段和数据段,然后让进程从新的代码段第一条指令开始执行,但进程id不变,换核不换壳。
exec函数族有6个以exec开头的函数,统称为exec函数族
#include <unistd.h>
int execl(const char *pathname, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *pathname, const char *arg, ..., char * const envp[]);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[]);
常用的有两种:execl和execlp:
exec示例
//exec_hello.c文件
int main(int argc, char* argv[])
{
printf("hello my pid is %d\n",getpid());
while(1);
return 0;
}
//exec_test.c文件
int main(int argc, char* argv[])
{
sleep(10);
printf("main pid: %d\n",getpid());
execl("./hello","./hello",NULL);
printf("我被覆盖了吗\n");
return 0;
}
第一点:查看pid是否更改
结果:没有改变
第二点:添加printf("我被覆盖了吗\n");查看是否execl后文被覆盖
结果:被覆盖了
第三点:检测运行的名字是否发生改变
在exec_test.c的首行添加sleep(10),等待十秒,便于观察,执行
执行exec_test程序时,我们会在该程序等待10秒,等待时使用ps -ajx |grep 命令查看进程,第一行就是我们需要的结果
这块我还没截图就不小心把上次的程序终止了,所以我重新开了一个,pid不一样的原因就是这个。但是现象是一样。我们只能使用pid查到新的名字hello,找不到exec_test了
综上:进程名字会改变
execlp示例
在程序内执行中端命令行指令
课后练习:
1.写一个程序:完成ls / >> a.txt指令的功能
int fd = open("a.txt",O_RDWR |O_CREAT |O_TRUNC,0664);
dup2(fd,STDOUT_FILENO);//重定向
execlp("ls","ls","/",NULL);
2.写一个程序:获取指定目录下文件的个数
const char *filename = "a.txt";
int pid=fork();
if(pid==0){
int fd=open(filename,O_RDWR |O_CREAT |O_TRUNC,0664);
dup2(fd,STDOUT_FILENO);
execlp("ls","ls","-a",argv[1],NULL);
} else{
wait(NULL);//等待子进程将内容写到文件中
execlp("wc","wc","-l",filename,NULL);
}
感谢大家!!!