[Linux] 初识进程(上)


旧识铺垫

我们之前学过冯 · 诺依曼奠定了现代计算机的硬件体系,目前现代计算机的五大硬件单元:

  1. 运算器
  2. 控制器
  3. 存储器:内存等
  4. 输入设备:键盘等
  5. 输出设备:显示器等

所以这就引出了我们的一句概念:硬件结构决定软件行为

比如运行某一程序,因为程序代码是保存在外部存储器中的,而使他运行操作系统就要从外存读入代码到内存中,再在内存中创建进程,给它分配运行必须的资源,然后插入就绪队列中。这就是一个硬件结构决定软件行为的典例。

读取速度由中央处理器CPU决定。
再细化为CPU的主频:时钟振荡周期,一秒钟可以处理多少个指令,值越大,系统吞吐量越大。

操作系统

那么操作系统是什么?

  • 其实质是一个软件
  • 目的:让计算机更好用
  • 功能:统筹管理计算机上的软硬件资源
  • 如何管理:先描述,再组织

操作系统要对硬件进行管理,首先要知道他们的描述信息。操作系统不亲自收集这些信息,而是驱动程序(中间执行者)来操作,操作系统通过驱动程序管理硬件信息

lib:用户不能直接接触操作系统,为了把风险降到最低,操作系统提供了一套系统调用接口供外部用户使用,完成某些功能,但是这些接口不太好用。
SHELL:为了更加好用,所以就封装成了一些shell,用户通过命令就可以操作操作系统。还有lib,用户使用库函数调用。

调用情况如下:
[Linux] 初识进程(上)

库函数(lib)和系统调用接口的关系:上下级的调用关系,库函数就是对系统调用接口的一层封装

操作系统:对下管理软硬件资源,对上提供良好的执行环境
操作系统的定位就是:搞管理的软件。


进程概念

  • 进程:进行(运行)中的程序。
  • 程序:位于硬盘的一堆代码/指令集。

操作系统通过PCB来管理运行中的程序(进程)。

  • PCB:进程控制块(process control block) ,Linux中实质是一个结构体,名为task_struct。其中包含了很多描述信息:

    时间片:cpu在一个进程上运行的时间
    进程标示符: 描述本进程的唯一标示符,用来区别其他进程。
    状态: 任务状态,退出代码,退出信号等。
    优先级: 相对于其他进程的优先级。
    程序计数器: 程序中即将被执行的下一条指令的地址。(存储即将处理的指令)
    内存指针: 包括程序代码和进程相关数据的指针,还有和其他进程共享的内存块的指针
    上下文数据: 进程执行时处理器的寄存器中的数据[休学例子,要加图CPU,寄存器]。
    I/O状态信息: 包括显示的I/O请求,分配给进程的I/O设备和被进程使用的文件列表。
    记账信息: 可能包括处理器时间总和,使用的时钟数总和,时间限制,记账号等。某些进程在CPU上运行了多长时间。。
    

磁盘中每一个程序执行时都会加载到内存中,就会产生此进程的PCB,其中包含的文件指针指向此时内存中的进程(及上下文数据等),CPU通过PCB去调度进程。

【注】CPU的分时机制:轮询调度进程

所以进程是什么要从两个角度来看:

  • 用户角度:进程就是运行中的程序。
  • 操作系统角度:操作系统运行一个程序,需要去描述这个程序的运行过程,而这个描述是通过一个结构来描述task_struct,统称PCB,因此对OS来说进程就是PCB

进程查看

  1. /proc 进程运行信息存放目录

  2. ps -elf/aux 查看系统上进程信息 (更加详细)
    (注:ps -auxps aux是不同的)

  3. 在程序中使用接口 getpid():获取调用进程的进程ID
    例如:printf("pid = [%d]",gitpid());

    tty 查看终端号


进程创建

fork()

使用这个函数需要包含头文件<unistd.h>

函数原型:

pid_t fork(void)

fork()通过复制调用进程,创建一个新的进程(子进程),子进程复制的就是父进程的PCB,但程序标识符pid不同。

父子进程数据,代码看起来都一样 ,但代码共享,数据独有。这涉及写时拷贝技术,我们在下篇中详细介绍。

创建子进程的意义

  1. 压力分摊
  2. 干其他工作

创建流程
进程调用fork,当控制转移到内核中的fork代码后,内核进行操作:

  1. 分配新的内存块和内核数据结构给子进程。
  2. 将父进程部分数据结构内容拷贝至子进程 (给子进程创建PCB,然后拷贝父进程数据进入)
  3. 添加子进程到系统进程列表汇总。
  4. fork返回,开始调度器调度。

getpid()函数对于父进程中返回子进程的pid,对于子进程返回 0,失败返回 -1
所以父子进程的区分标准就是返回值,由此引出下面代码:

代码实例

int main(){
	printf("parent pid:%d\n",getpid());		//通过getpid接口获取调用进程的pid
	pid_t pid = fork();
	if(pid < 0){	//创建失败
		return -1;
	}
	else if(pid == 0){
		printf("child子进程\n");
	}
	else{
		printf("parent父进程\n");
	}
}

vfork()

创建子进程,并阻塞父进程。

  1. fork创建pcb并开辟内存空间,vfork创建的子进程有自己的PCB,但和父进程共用同一块虚拟空间
  2. 子进程先运行,父进程等到子进程exit退出或者程序替换后,父进程才运行。
  3. 否则同时运行的话,因为父子进程共用虚拟地址空间,会造成调用栈混乱,因此需要阻塞父进程。
    (比如父进程调用了memcpy函数)
  4. vfork子进程如果调用return退出,会释放资源,会导致父进程陷入混乱,或者错误。
  5. 子进程不终止,父进程一直处于阻塞状态。

fork/vfork 与 clone

vfork函数相对于早期fork函数:略过开辟空间、拷贝数据的过程,现在fork函数的写时拷贝技术也避免了无用的拷贝,所以vfork接口使用形式式微。

比较
fork/vfork都是库函数接口,clone是系统调用接口。

  • fork/vfork都是调用的是clone函数

【注】换行符\n的作用:

  1. 换行
  2. 刷新缓冲区

在本篇博客的结尾给出进程初识阶段(下篇)博客:
https://blog.****.net/qq_42351880/article/details/89043560
有兴趣的看官大佬可以继续了解一下~