Linux进程的概念
一、进程
1.基本概念
课本概念:程序的一个执行实例,正在执行的程序等
内核观点:担当分配系统资源(CPU时间,内存)的实体。
2.描述进程(PCB)
实际上,我们所写的代码运行起来就是进程,如何管理进程呢?
先描述,再组织;
任何进程在形成之时,操作系统要为该进程创建PCB(进程控制模块);简单的讲,PCB就是一个结构体(Linux操作系统下的PCB是:struct task_struct),里面存放了进程相关的属性信息
3.进程与程序的区别
首先,我们编写好的程序,在经过编译处理之后,所产生的文件(可执行程序)会放在磁盘中,当我们运行程序时,操作系统将磁盘上的文件加载到内存,同时为它创建PCB(进程控制块),这两个组合起来才是进程
4.task_struct-PCB的一种
在Linux中描述进程的结构体叫做task_struct
task_struct是Linux内核的一种数据结构,它会被装载到RAM(内存)里并且包含着进程的信息
5.task_struct的内容分类
- 标识符:描述本进程的唯一标识符,用来区分其他进程
- 状态:任务状态,退出代码,退出信号等。
- 优先级:相对于其他进程的优先级
- 程序计数器:程序中即将被执行的下一条指令的地址
- 内存指针:包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
- 上下文数据:进程执行时处理器的寄存器中的数据
- I/O状态信息:包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
- 记账信息:可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
二、如何查看进程
1.通过系统文件查看进程
通过系统文件/proc,来查看当前所有的进程
2.通过ps指令查看进程
ps指令用于报告当前系统的进程状态。可以搭配kill指令随时中断、删除不必要的程序。ps命令
是最基本同时也是非常强大的进程查看命令,使用该命令可以确定有哪些进程正在运行和他们运行的状态、进程是否结束、进程有没有僵死、哪些进程占用了过多的资源等等,总之大部分信息都是可以通过执行该命令得到的。
ps axj //显示现行终端机下的所有进程,包括其他用户的进程
ps axj | head -1 && ps axj | grep -v grep | grep "a.out" //显示某一个进程的信息
三、如何获取pid和ppid
我们想要获取到pid和ppid,就要用到系统调用接口:
pid_t getpid(void) —返回的是子进程的ID
pid_t getppid(void) —返回的是父进程的ID
我们可以通过man指令对这些函数进行详细说明的查看
1.getpid() — 获取子进程(pid)
#include<stdio.h>
#include<unistd.h>
int main(){
printf("I am child pid: %d\n",getpid());
return 0;
}
2.getppid() —获取父进程(ppid)
#include<stdio.h>
#include<unistd.h>
int main(){
printf("I am father pid : %d\n",getppid());
return 0;
}
这里将两个代码整合到一起后,通过死循环不断的打印有父子进程的ID,并对进程进行检测,发现ps检测到的ID和系统接口获取到的ID是一样的。
如果想要终止左边的死循环程序有两个办法
- ctrl+c
- kill -9 5717
四、进程的创建 — fork初识
1.四种主要事件会导致进程的创建
- 系统初始化
- 正在运行的程序执行了创建程序的系统调用
- 用户请求创建一个新进程
- 一个批处理作业的初始化
2.用户如何请求创建一个新进程
通过fork函数来进行进程的创建,我们可以man fork查看相关的函数信息。这是一个系统调用,它会创建一个与调用进程相同的副本。在调用fork之后,这两个进程(父进程和子进程)拥有相同的内存映射
请看下面这段代码,我们在执行循环前创建了一个子进程,会有什么样的效果呢?
#include<stdio.h>
#include<unistd.h>
int main()
{
fork();//创建子进程
while(1){
printf("I am child pid:%d I am father ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
从上述的结果可以看出,main函数的进程和fork创建的进程打印的结果是一样的,并且通过pid和ppid发现,fork的父进程就是main函数的进程,说明fork所创建出来的子进程是父进程在内存上映射
3.如何让父子进程各有所需
以上创建的子进程所做的事和父进程是一样的,显然意义并不大,我们要能够让所创建的子进程
做和父进程不一样的事,才是我们想要的。那如何实现呢?
首先,对于fork是有两个返回值的
1.如果子进程创建成功,在父进程中返回子进程的PID,而在子进程中返回0
2.如果子进程创建失败,则在父进程中返回-1。
#include<stdio.h>
#include<unistd.h>
#include<string.h>
#include<stdlib.h>
int main(){
pid_t id=fork();
if(id<0){
perror("fork fail");
exit(-1);
}
else if(id==0){
//child process
printf("I am child process\n");
exit(0);//child process exit
}
//parent process
printf("I am parent process\n");
return 0;
}
五、进程的状态
1.进程的状态有哪些
CPU对进程处理,取决于进程当前进程所处的状态,CPU对于不同状态的进程会采取不同的措施
/*
* The task state array is a strange "bitmap" of
* reasons to sleep. Thus "running" is zero, and
* you can test for combinations of others with
* simple bit tests.
*/
static const char * const task_state_array[] = {
"R (running)", /* 0 */ //不可中断
"S (sleeping)", /* 1 */ //正在运行,或在队列中的进程
"D (disk sleep)", /* 2 */ //处于休眠状态
"T (stopped)", /* 4 */ //停止或被追踪
"t (tracing stop)", /* 8 */ //追踪状态,类似于vs下打断点后直接运行到断点处
"X (dead)", /* 16 */ //死掉的进程
"Z (zombie)", /* 32 */ //僵尸进程
};
2.进程状态的查看
ps axj / ps aux
3.进程状态的分析
1.运行状态(R)
R运行状态(running):并不意味着进程一定在运行中,它表明进行要么是在运行中要么是在运行队列里面。
上图想要表达的意思是,进程A处于运行中,在一段时间后,就会切换到进程B…,这个时间很快,CPU运行这些进程是采用了时间轮转调度算法。在时间片轮转调度算法中,系统根据先来先服务的原则,将所有的就绪进程排成一个就绪队列,并且每隔一段时间产生一次中断,激活系统中的进程调度程序,完成一次处理机调度,把处理机分配给就绪队列首进程,让其执行指令。当时间片结束或进程执行结束,系统再次将cpu分配给队首进程。
2.浅度睡眠状态(S)
S睡眠状态(sleeping):意味着进程在等待事件完成(这里的睡眠有时候也叫做可中断睡眠(interruptible sleep))
#include <stdio.h>
#include <unuistd.h>
int main(){
printf("hello linux!\n");
sleep(50);
return 0;
}
处于S状态的进程,是可以被立即终止的
3.深度睡眠状态(D)
D磁盘休眠状态(Disk sleep)有时候也叫做不可中断睡眠状态(uninterrupttible sleep),在这个状态的进程通常会等待IO的结束。例如,当进程要求对磁盘进行写入操作,那么在磁盘进行写入期间,该进程就处于深度睡眠状态,是不会被杀掉的,因为该进程需要等待磁盘的回复(是否写入成功)以做出相应的应答。(磁盘休眠状态)
4.停止状态(T)
T停止状态(stopped):可以通过发送SIGSTOP信号给进程来停止(T)进程。这个被暂停的进程可以通过发送SIGCONT信号让进程继续运行。
#include <stdio.h>
#include <unuistd.h>
int main(){
while(1){
printf("hello linux!\n");
sleep(1);
}
return 0;
}
在上面的程序跑起来之后,处于浅度睡眠状态,我们可以发送SIGSTOP信号给进程来停止(T)进程;
我们发送SIGSTOP信号给进程来停止(T)进程后,还需要发送SIGCONT信号让进程继续运行。
查看kill相关信号:
kill -l
以上的信号,可以查看教程后,尝试尝试
5.僵死状态(Z)
僵死状态(Zombies)是一个比较特殊的状态。当子进程退出并且父进程没有读取到子进程退出的返回代码时就会出现僵死(尸)进程,僵死进程会以终止状态保持在进程表中,并且会一直在等待父进程读取退出状态码。所以,只要子进程退出,父进程还在运行,但父进程没有读取子进程状态,子进程就会进入Z状态;
6.死亡状态(X)
X死亡状态(dead):死亡状态只是一个返回状态,当一个进程的退出信息被读取后,该进程所申请的资源就会立即被释放,该进程也就不存在了,所以你不会在任务列表当中看到死亡状态
六、僵尸进程与孤儿进程
1.僵尸进程的危害
有如下代码,我们执行之后,子进程会不断的打印数据,父进程等待子进程的过程中,我们立刻杀掉子进程,那么子进程就会处于僵尸状态,而此时程序还在运行,父进程在等待子进程退出的状态,我们把这种进程称之为僵尸进程。
#include <stdio.h>
#include <unistd.h>
int main(){
int ret = fork();
if(ret == 0){//子进程一直打印
while(1){
printf("hello linux!\n");
sleep(1);
}
}
else{ //parent process do nothing,just sleep
sleep(100);
}
return 0;
}
僵尸进程的危害:
1.进程的退出状态必须被维持下去,因为它要告诉关心它的进程(父进程),你交给我的任务,我办的怎么样了。可父进程如果一直不读取,那子进程就一直处于Z状态?
是的!
2.维护退出状态本身就是要用数据维护,也属于进程的基本信息,所以保存task_struct(PCB)中,换句话说,Z状态一直不退出,PCB一直都要维护?
是的!
3.那一个父进程创建了很多子进程,就是不回收,是不是就会造成内存资源的浪费?
是的!因为数据结构对象本身就要占用内存,想想C中定义一个结构体变量(对象),是要在内存的某个位置进行开辟空间!
4.内存泄漏?
是的!
2.孤儿进程
刚刚提到了僵尸进程是由于子进程先退出而父进程没有对子进程的退出信息进行读取;那么父进程先退出,子进程在进入僵尸状态后,其父进程未能对其做出处理,那么就称该进程是孤儿进程。若是一直不处理孤儿进程的退出信息,那么孤儿进程进入僵尸状态时就由1号进程进行处理回收。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(){
pid_t ret = fork();
if(ret == 0){//子进程不断的打印数据
while(1){
printf("I am child, running!\n");
sleep(1);
}
}
else{//父进程打印数据,休眠10秒后,直接退出
printf("father do nothing!\n");
sleep(10);
exit(1);
}
return 0;
}
子进程执行的过程中,父进程未退出时,父子进程都是S+状态
子进程执行的过程中,父进程退出了,子进程被1号进程领养了(1号进程:操作系统)