进程与线程
- 进程是计算机中处于运行中程序的实体;每个进程都有独立的代码和数据空间,进程间的切换会有较大的开销。
- 线程是CPU执行的最小单元,进程是线程的容器;同一类线程共享代码和数据空间,每个线程有独立的运行栈和程序计数器(PC),线程切换开销小。
多进程与多线程
- 多进程是指操作系统能同时运行多个任务(程序);
- 多线程是指在一个程序中有多个顺序流在执行。
程序与进程
进程结构
进程结构一般由3部分组成:代码段、数据段、堆栈段
- 代码段:用于存放程序代码的数据。假如机器中有多个进程运行相同的一个程序,那么他们就可以使用同一个代码段。
- 数据段:用于存放程序的全局变量、常量和静态变量。
- 堆栈段:堆栈段中的栈用于函数调用,存放函数参数、函数内部定义的局部变量。
- 堆栈段还包括了进程控制块(PCB):PCB处于进程核心堆栈的底部,是进程存在的唯一标识,系统通过PCB对进程进行管理和调度。
程序的生成
Linux下C++程序的生成分为4个阶段:预编译、编译、汇编、链接。编译器g++经过预编译、编译、汇编3个步骤将源程序文件转换为目标文件。如果程序有多个目标文件或使用了库函数,编译器还需要将所有的目标文件或所需的库链接起来,最后形成可执行程序。
程序转换为进程
程序本身只是指令、数据以及组织形式的描述,进程才是程序真正的运行实例。即:所谓程序,不过是指可运行的二进制代码文件,把这种文件加载到内存中运行就得到了一个进程。
所谓的程序,指的是可运行的二进制代码文件,把这种文件加载到内存中运行就得到了一个进程。进程与进程标识符是一对一的关系,而与程序文件是多对一的关系(同一个程序文件可以被加载多次成为不同的进程)。
一般程序转换为进程分为以下几个步骤:
- 内核将程序读入内存,为程序分配内存空间。
- 内核为该进程分配进程标识符(PID)和其他所需资源。
- 内核为进程保存PID及相应的状态信息,把进程放到运行队列中等待执行,程序转化为进程后就可以被操作系统的调度程序调度执行了。
进程的创建与结束
进程的创建有两种方式:一种是由操作系统创建,一种是由父进程创建。在Linux系统中,除了系统启动之后的第一个进程由系统来创建,其余进程由已存在的进程创建,形成一个树形结构。
- 树根是由系统自动构造的,即在内核态下执行的0号进程,它是所有进程的祖先。
- 由0号进程创建1号内核态进程,1号负责执行内核的部分初始化工作及进行系统配置,并创建若干个用于高速缓存和虚拟贮存管理的内核线程。
- 随后,1号进程调用execve()运行可执行程序init,并演变为用户态1号进程,即init进程。
- 1号进程会创建编号为1号、2号…的若干终端注册进程getty,每个getty进程设置其进程组标识号,并检测配置到系统终端的接口线路。
- 当检测到来自终端的连接信号时,getty进程将通过函数execve()执行注册程序login,此时用户就可以输入注册名和密码进入登录过程。
- 登录成功后,由login程序再通过函数execv()执行shell,该shell进程1接收getty进程的pid,取代原来的getty进程。
- 再由shell直接或间接地产生其他进程。
综上:0号进程——>1号内核进程——>1号内核线程——>1号用户进程(init进程)——>getty进程——>shell进程
1. 进程的创建——fork()函数
Linux系统下使用fork()函数创建一个子进程,其函数原型如下:
#include<unistd.h>
pid_t fork(void);
fork函数的返回值有三种情况:
- 对于父进程,fork()函数返回新创建的子进程的ID;
- 对于子进程,fork()函数返回0;
- 如果创建出错,fork()函数返回-1.
fork()函数会创建一个新的进程,并从内核中为此进程分配一个新的进程标识符(PID),之后,为这个新进程分配进程空间,并将父进程的内容复制到子进程的进程空间中,包括父进程的数据段和堆栈段,但是代码段是只读的,不存在被修改的问题,所以共用代码段。
由于在复制时复制了父进程的堆栈段,所以两个进程都停留在fork()函数中,等待返回。因此,fork()函数会返回两次,一次是在父进程中返回,另一次是在子进程中返回,这里两次的返回值时不一样的。
代码分析:
《后台开发:核心技术与应用实践》Page_337:例10.1“创建一个子进程”
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
perror("fail to fork");
exit(-1);
}
else if (pid == 0) //子进程
printf("Sub-process, PID: %u, PPID: %u\n", gepid(), getppid());
else //父进程
{
printf("Parent, PID: %u, Sub-process PID: %u\n", getpid(), pid);
sleep(2);
}
return 0;
}
由于创建的新进程和父进程在系统看来是地位平等的两个进程,运行机会也是一样的,故不能对其执行先后顺序进行假设,先执行哪一个进程取决于系统的调度算法。getpid()
是获得当前进程的PID,getppid()
是获得父进程的PID。
现代操作系统的“写时复制”概念:
现在的Linux内核在实现fork()函数时往往在创建子进程时并不会立即复制父进程的数据段和堆栈段,而是当子进程修改这些数据内容时复制操作才会发生,内核才会给子进程分配进程空间,将父进程的内容复制过来,然后继续后面的操作。这样的实现更加合理,对于一些只是为了复制自身完成一些工作的进程来说,这样做的效率更高。
2. 进程的结束——exit()函数
Linux中进程退出分为正常退出和异常退出两种:
- 正常退出:(1)在main()函数中执行return;(2)调用exit()函数;(3)调用_exit()函数。
- 异常退出:(1)调用abort函数;(2)进程收到某个信号,而该信号使程序终止。
exit()函数是在头文件<stdlib.h>
中声明,而_exit()在头文件<unistd.h>
中声明。exit()中的参数为0时代表进程正常终止,若为其他值表示程序执行过程中有错误发生。
-
exit和return区别:
1.exit是一个函数,带有参数,exit执行后把控制权交给系统;
2.return是函数执行完后的返回,return执行完后把控制权交给调用函数。 -
exit和abort的区别:
1.exit是正常终止进程;
2.abort是异常终止。
exit()和_exit():
- exit和_exit都是用来终止进程的。
当程序执行到exit或_exit时,系统无条件地停止剩下所有操作,清除包括PCB在内的各种数据结构,并终止本程序的运行。exit()在头文件stdlib.h中声明,_exit()声明在头文件unistd.h中声明。
- _exit()执行后立即返回给内核,而exit()要先执行一些清除操作,然后将控制权交给内核。
在调用_exit函数时,其会关闭进程所有的文件描述符,清理内存以及其他一些内核清理函数,但不会刷新流(stdin、stdout、stderr…)的数据。exit函数时在_exit函数之上的一个封装,其会自动调用_exit,并在调用之前先刷新数据流。
- exit()函数和_exit()函数最大的区别在于exit()函数在调用exit系统之前要检查文件的打开情况,把文件缓冲区的内容写回文件。
比如有一些数据理论上应该已经写入了文件,但实际上因为没有满足特定的条件(如换行符),它们还只是保存在缓冲区内,这时如果用_exit()函数直接将进程关闭,缓冲区的数据就会丢失。因此要想保证数据的完整性,就一定要使用exit()函数。
shell作为一种和Linux系统的特殊交互式工具,为用户提供了启动程序、管理文件系统中的文件及运行在Linux上的进程的途径。shell通过解析输入的文本命令,在内核中执行来达到与系统交互的功能。shell包含了一组内部命令,通过这些命令可以进行文件管理、程序管理及运行等操作。 ↩︎