目录
一、进程概念
课本概念:进程是程序的一个执行实例,是正在执行的程序。
内核观点:进程是承担系统资源(CPU时间、内存)的实体。
当我们写完代码之后,编译连接就形成一个可执行程序.exe,本质是二进制文件,在磁盘上存放着。双击这个.exe文件把程序运行起来就是把程序从磁盘加载到内存,然后CPU才能执行其代码语句。当把程序加载到内存后,这个程序就叫做进程。所有启动程序的过程,本质都是在系统上创建进程,双击.exe文件也不例外:
二、PCB
1.什么是PCB
根据操作系统管理是先描述再组织,那么操作系统是如何描述进程的呢?先预想一下,肯定是先描述进程信息,然后再把这些信息用数据结构组织起来进行管理。那么进程都有哪些信息呢?使用
ps axj
命令查看系统当中的进程,也就是正在运行的程序:
看到进程的属性至少有PPID、PID、PGID、SID、TTY、TPGID、STAT、UID、TIME、COMMAND。
进程信息被放在一个叫做进程控制块PCB(Process Control Block)的数据结构中,它是进程属性的集合。
操作系统创建进程时,除了把磁盘上的代码和数据加载到内存以外,还要在系统内部为进程创建一个task_struct,是一个struct。
2.什么是task_struct
Linux操作系统的下的PCB就是task_struct,所以task_struct是PCB的一种,在其他操作系统中的PCB就不一定叫task_struct。
创建进程不仅仅把代码和数据加载到内存,还要为进程创建task_struct,所以进程不仅仅是运行起来的程序,更准确的来说,进程是程序文件内容和操作系统自动创建的与进程相关的数据结构,其实进程还包括其他内容,今天先说这两个。
操作系统对每一个进程进行了描述,这就有了一个一个的PCB,Linux中的PCB就是task_struct,这个struct会有next、prev指针,可以用双向链表把进程链接起来,task_struct结构体的部分指针也可以指向进程的代码和数据:
所有运行在系统里的进程,都以task_struct作为链表节点的形式存储在内核里,这样就把对进程的管理变成了对链表的增删改查操作。
增:当生成一个可执行程序时,将.exe文件存放到磁盘上,双击运行这个.exe程序时,操作系统会将该进程的代码和数据加载到内存,并创建一个进程,对进程描述以后形成task_struct,并把插入到双向链表中。
删:进程退出就是将该进程的task_struct节点从双向链表中删除,操作系统把内存中该进程的代码和数据进行释放。
3.task_struct包含内容
标示符: 描述本进程的唯一标示符,用来区别其他进程。
状态: 任务状态,退出代码,退出信号等。
优先级: 相对于其他进程的优先级。
程序计数器: 程序中即将被执行的下一条指令的地址。
内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针。
上下文数据: 进程执行时处理器的寄存器中的数据。
I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。
还有一些其他信息。下面解释task_struct包含内容的具体含义。
三、task_struct内容详解
1.查看进程
(1)通过系统目录查看
proc是一个系统文件夹,在根目录下,通过ls可以看到该文件夹:
可以通过
ls /proc
命令查看进程的信息,数字是PID:
如果想查看进程信息,比如查看PID为989的进程信息,使用命令
ls /proc/PID
查看:
(2)通过ps命令查看
使用
ps aux
命令查看进程,可以看到所有进程:
如果结合grep可以查看某一个进程:
比如想查看包含proc的进程,可以使用如下命令:
ps aux | head -1 && ps aux | grep proc | grep -v grep
(3)通过top命令查看
也可以通过
top
命令查看:
(4)通过系统调用获取进程PID和父进程PPID
-
获取进程ID函数getpid和getppid
获取进程ID和获取父进程ID可以通过以下方式进行获取,其中pid_t是short类型变量:
#include <sys/types.h>
#include <unistd.h>
pid_t getpid(void);//获取当前进程ID
pid_t getppid(void);//获取当前进程的父进程ID
-
获取当前进程ID
获取当前进程,process.c
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("hello linux!:pid:%d\n",getpid());//获取当前进程ID
sleep(1);
}
return 0;
}
Makefile:
process:process.c
gcc -o $@ $?
.PHONY:clean
clean:
rm -f process
运行之后,就获取到了当前进程的PID,即进程号:
关闭进程可以通过ctrl+c或者来关闭进程。另开一个窗口,现在通过ps来查看进程:
这也就验证了getpid获取到的是PID。
-
获取父进程ID
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>
int main()
{
while(1)
{
printf("hello linux!:pid:%d,ppid:%d\n",getpid(),getppid());
sleep(1);
}
return 0;
}
使用ps命令查看,发现父进程的ID是11081,但是11081同时也是bash的子进程:
这是因为,运行命令行的命令有风险,命令行出错了,不能影响命令行解释,因此在命令行上运行的命令,基本上父进程都是bash。
使用如下命令可查看到进程内部的所有属性信息:
ls /proc/当前进程ID -al
当进程退出时,就没有/proc/18448这个文件夹了,ctrl c后,再去查看文件夹,已经不存在了:
2.状态
之前写代码的返回值是0 ,这个0是进程退出时的退出码,这个退出码是要被父进程拿到的,返回给系统,父进程通过系统拿到。比如以下代码的退出码是0
#include<stdio.h>
int main()
{
printf("hello linux!\n");
return 0;
}
那么使用
echo $?
就可以查看到进程退出码为0:
假如将退出码改为99 :
那么程序运行后的退出码也变成了99:
所以,状态的作用是输出最近执行的命令的退出码。
3.优先级
权限指的是能不能,而优先级指的是已经能了,有权限了,但是至于什么时候执行得先排队 ,这就像在餐馆点餐结帐出小票之后,已经可以拿到餐食了,但是什么时候能拿到呢?需要排队,在这个过程中,是否出小票就代表是否有权限,排队取餐就代表的是优先级。
4.程序计数器
当CPU执行程序时,执行当前行指令时,怎么知道下一行指令是什么呢?程序计数器pc中存放下一条指令的地址,当操作系统执行完当前行指令后,pc自动会++,直接执行下一行命令。
内存指针可以通过task_struct中的内存指针,通过PCB 找到进程的代码和数据。
5.上下文数据
当操作系统维护进程队列时,由于进程代码可能不会在很短时间就能执行完毕,假如操作系统也不会在执行一个进程时,让其他进程一直等待,直到当前进程执行完毕,那可能当前进程需要执行很久才执行完毕,其他进程会一直处于等待状态,这不合理。那么操作系统在实际执行进程调度时,按时间片分配执行时间,时间片一到,就切换下一个进程。时间片是一个进程单次运行的最长时间。
比如有4个进程,在40ms之内先让第一个进程运行10ms,时间一到就算没有运行完毕,就把第一个进程从队列头移动到队列尾,再让第二个进程运行10ms。40ms后,使得用户感知到这4个进程都推进了,其实本质上是通过CPU的快速切换完成的。
有可能在一个进程的生命周期内被调度成百上千次。比如CPU有5个寄存器,进程A正在运行时时间片到了,被切走的时候,会把CPU里和进程A相关的保存到寄存器里面的临时数据带走。当进程B调度完后,再次调度进程A的时候,会把进程A里面保存的临时数据再恢复到CPU的寄存器当中,继续上次切走时的状态继续运行,因此保护上下文能够保证多个进程切换时共享CPU。
6.I/O状态信息
文件操作有fopen、fclose、fread、fwrite等函数,其实是进程在操作文件,因为在把代码写完之后,程序运行起来时,操作系统会找到这个进程,进程打开文件进行IO操作,其实IO都是进程在进行IO,所以操作系统需要维护进程和IO信息。
7.记账信息
记录历史上一个进程所享受过的软硬件资源的结合。
四、通过系统调用创建进程
1.使用fork创建子进程
fork用来创建子进程:
#include <unistd.h>
pid_t fork(void);//通过复制调用进程创建一个新进程。新进程称为子进程。调用进程称为父进程。
先看一个奇奇怪怪的代码:
forkProcess_getpid.c
#include<unistd.h>
#include<stdio.h>
int main()
{
int ret = fork();
if(ret > 0)
{
printf("I am here\n");
}
else
{
printf("I am here,too\n");
}
sleep(1);
return 10;
}
按道理来说,要么打印I am here,要么打印I am here,too。但是请看执行结果,发现两句话都打印了,也就是既执行了if又执行了else:
再看代码:
#include<stdio.h>
#include<unistd.h>
int main()
{
int ret = fork();
while(1)
{
printf("I am here,pid = %d,ppid = %d\n",getpid(),getppid());
sleep(1);
}
return 10;
}
发现有两个pid和ppid:
这说明执行while死循环不只一个执行流在执行, 而是两个执行流在执行,每一行两个id都是父子关系。这是因为fork之后有两个执行流同时执行while循环。
可以看到bash 16202创建了子进程 16705,子进程又创建了子进程 16706: