在多用户,多任务的操作系统中,程序都是以进程的形式运行的。
7.1 进程基本概念
7.1.1 进程与PCB
进程的引入是为了更好的描述和控制程序的并发执行,体现操作系统的并发性和共享性。进程是一个实体,由程序段,相关数据段和PCB三部分组成进程映像,所谓创建进程,就是创建进程映像的PCB,撤销进程,就是撤销进程的PCB;进程是一个“执行中的程序”,进程映像是静态的,进程是动态的。
也就是说,PCB是进程存在的唯一标志。
进程实体在数据集合上的一次运行过程,是拥有资源和分派资源的基本单位。在传统的操作系统中,进程即是基本的分配单元,也是基本的执行单元。
注意,这里的资源主要应当是处理机的时间片。
Linux系统中,要运行一个程序,必须由Linux为此程序创建进程。对多任务系统,内存中会同时驻留多个进程,多进程“并发”执行。
只有当某个进程获得其运行所需要的所有资源(包括CPU),它才能够执行;当进程终止后,Linux还要做一些资源收集和清理工作,从队列中删除PCB。
在进程创建时,操作系统为其新建一个PCB,该结构之后常驻内存,并在进程结束后删除。进程执行时,系统通过PCB了解进程的现行状态信息以便对其控制和管理。操作系统通过PCB表来管理和控制进程。
PCB:struct task_struct,其中记录的信息包括:进程标识符,相关的文件描述符,进程状态,接收的信号等。
7.1.2 进程标识pid
每个进程都对应一个唯一进程标识pid,是一个非负整数。其数据类型定义为pid_t非负整数。
最大的PID为32767,每次分配进程id从小开始分配,依次递增,直到最大值后开始使用空闲的最小pid。
Linux下有3个特殊的进程,idle进程(PID=0PID=0), init进程(PID=1PID=1)和kthreadd(PID=2PID=2)。
idle进程由系统自动创建, 运行在内核态。<br />idle进程其pid=0,其前身是系统创建的第一个进程,也是唯一一个没有通过fork或者kernel_thread产生的进程。这是系统启动过程中的第一个进程,也是**所有进程的祖先**。是系统启动过程中的第一个进程。
系统允许一个进程创建新进程,新进程即为子进程,子进程还可以创建新的子进程,形成进程树结构模型。整个linux系统的所有进程也是一个树形结构。树根是系统自动构造的(或者说是由内核黑客手动创建的),即在内核态下执行的0号进程,它是所有进程的远古先祖。
在smp系统中,每个处理器单元有独立的一个运行队列,而每个运行队列上又有一个idle进程,即有多少处理器单元,就有多少idle进程。
init进程由idle通过kernel_thread创建,在内核空间完成初始化后, 并最终调用execve函数加载init程序运行,演变为用户态1号进程。init进程由0进程创建,完成系统的初始化. 是系统中所有其它用户进程的祖先进程。
Linux中的所有进程都是有init进程创建并运行的。首先Linux内核启动,然后在用户空间中启动init进程,再启动其他系统进程(shell进程)。在系统启动完成完成后,init将变为守护进程监视系统其他进程,作为众多孤儿进程的新的父进程,从而回收那些进程终止后遗留的资源。
但是这里我们发现一个问题, init进程应该是一个用户空间的进程, 但是这里却是通过kernel_thread的方式创建的, 哪岂不是式一个永远运行在内核态的内核线程么, 它是怎么演变为真正意义上用户空间的init进程的?
1号kernel_init进程完成linux的各项配置(包括启动AP)后,就会在/sbin,/etc,/bin寻找init程序来运行。该init程序会替换kernel_init进程(注意:并不是创建一个新的进程来运行init程序,而是一次变身,使用sys_execve函数改变核心进程的正文段,将核心进程kernel_init转换成用户进程init),此时处于内核态的1号kernel_init进程将会转换为用户空间内的1号进程init。户进程init将根据/etc/inittab中提供的信息完成应用程序的初始化调用。然后init进程会执行/bin/sh产生shell界面提供给用户来与Linux系统进行交互。
调用init_post()创建用户模式1号进程。
kthreadd进程由idle通过kernel_thread创建,并始终运行在内核空间, 负责所有**内核线程**的调度和管理<br />它的任务就是管理和调度其他内核线程kernel_thread, 会循环执行一个kthread的函数,该函数的作用就是运行kthread_create_list全局链表中维护的kthread, 当我们调用kernel_thread创建的内核线程会被加入到此链表中,因此所有的内核线程都是直接或者间接的以kthreadd为父进程。
注意,上述过程描述中提到:1号内核进程调用执行init函数并演变成1号用户态进程(init进程),这里前者是init是函数,后者是进程。两者容易混淆,区别如下:
kernel_init函数在内核态运行,是内核代码
init进程是内核启动并运行的第一个用户进程,运行在用户态下。
一号内核进程调用execve()从文件/etc/inittab中加载可执行程序init并执行,这个过程并没有使用调用do_fork(),因此两个进程都是1号进程。
系统第一个进程为init进程,pid为1,以后所有进程都是通过fork产生的子/孙进程。 pstree命令
除init进程外,每个进程都有由另一个进程创建出来的,该进程称为被创建进程的父进程,而且每个进程都有唯一的父进程(父进程标识ppid)。此外,进程还属于某个进程组中。
7.1.3 用户标识
/etc/passwd文件保存用户ID,组ID,用户信息。/etc/group文件中保存了组的信息。
进程是通过UID和GID来控制用户对对文件等资源的访问权限的。
用户不能修改其用户ID,组ID。
进程的实际用户ID是用来标识是谁来执行进程,一般来说是登录用户;而有效用户ID则表示该进程的访问权限。假设某进程的实际用户为u1,而实际用户为u2,则该进程可以访问u2用户可以访问的文件,但是不能访问u1用户的文件。
setuid位、setgid位设置后使执行者访问系统资源时具有文件拥有者或组的身份。
7.2 进程控制
7.2.1 创建进程
子进程拥有父进程的数据空间、堆、栈的一个拷贝,父子进程不共享这部分存储空间。系统调用fork的实现并不把父进程的数据段、堆、栈完全复制到子进程中,而是使用写时复制技术(Copy on Write,COW),即父子进程共享这些存储空间,当某个进程修改这些存储空间时,才做一个拷贝。另外,调用fork之后一般调用exec*系列函数。
父子进程执行的先后顺序由操作系统的处理器调度策略决定,多数情况下,该策略会让新创建的进程得到一个时间片优先执行,从而子进程先于父进程执行;如果子进程执行的代码在一个时间片内无法执行完成,则之后的执行次序全凭处理器调度程序。
需要注意的是,子进程会继承父进程的缓冲区。当缓冲方式为全缓冲时(如磁盘输出到指定文件),缓冲区的内容会被父子进程都打印一份。
vfork与fork的区别在于vfork是借用而不复制父进程空间,之所以提出vfork就是由于fork写时复制的策略固然能减少全部拷贝浪费的时间空间,但是fork还是要将父进程在内存中的部分内容(数据空间)拷贝给子进程,这还是要浪费时间空间的。
所以如果创建子进程目的是在子进程空间中执行其他程序,那么数据空间也不需要拷贝。父子进程共享数据空间,父进程等待子进程先执行。因此使用vfork创建子进程,目的是子进程直接调用exec族函数去执行其他程序。
7.2.2 exec*系列函数
系统调用fork创建子进程时,父子进程拥有相同的代码段。如果希望子进程运行另外一个程序,则需要调用exec系列函数。exec系列函数不会创建新进程,而是用新进程替换子进程的地址空间,包括代码段,数据,堆,栈。而且,新进程pid保持不变。
(shell)
在exec*系列函数中,execve是一个系统调用,另外5个为库函数,也就是说,实现这些库函数的代码中调用了系统
调用execve。
l:list 列表,元组,包含可变参数,传递命令行参数;v:vector 数组,向量,一个字符串数组,传递命令行参数。
p:path;e:environment环境变量
7.2.3 等待进程结束
linux特殊进程:孤儿进程与僵死进程。
因父亲进程先退出而导致一个子进程被init进程收养的进程为孤儿进程。
而已经退出但还没有回收资源的进程为僵死进程。
当子进程终止时,内核会在该进程的PCB中标记进程的终止状态,以便其父进程能够获得其终止状态,意味着让系统释放与子进程相关的资源。如果子进程一直存在,父进程调用wait后就会挂起等待直到某个子进程状态改变。若父进程在中止前没有进行wait,那么已经终止运行的子进程并没有被完全销毁,处于僵死状态,其PCB仍然残留在系统中。
7.2.4 进程终止
进程正常终止:
- main函数中执行return函数
- 调用exit/_exit,exit是库函数,在系统调用exit后,c语言运行库会调用由atexit注册的函数,最后会调用_exit系统调用。
异常终止:
- 接受到某种信号
- 调用abort,产生SIGABRT信号,类似接收信号
atexit是注册终止处理函数,即由exit函数调用的函数,由于执行相关处理工作。
在以下运行过程中,func1和fun2函数的运行次序与atexit函数注册的次序正好相反。
1.exit
2._exit
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1635947893330-ddef74d7-c1a0-4a05-8ab0-742910b21797.png#clientId=u14bc0dc9-f42e-4&from=paste&height=290&id=u0766e0d2&margin=%5Bobject%20Object%5D&name=image.png&originHeight=579&originWidth=877&originalType=binary&ratio=1&size=66652&status=done&style=none&taskId=ua612aece-5f39-4374-86c4-024a8d3cd21&width=438.5)
3. exit和_exit区别
![image.png](https://cdn.nlark.com/yuque/0/2021/png/1054933/1635947974965-5e1d5352-8589-4020-89c0-526a0a8201c2.png#clientId=u14bc0dc9-f42e-4&from=paste&height=243&id=u22526ad3&margin=%5Bobject%20Object%5D&name=image.png&originHeight=486&originWidth=806&originalType=binary&ratio=1&size=58233&status=done&style=none&taskId=u00afd7c0-6eac-4f1c-b21f-2bc4faf1633&width=403)
7.2.5 system函数
7.3 什么是shell
shell是用c语言编写的程序,既是命令解释器,也是一种程序设计语言。
作为命令解释器,是操作系统内核与用户之间的重要接口。