今天开始整理记录linux操作系统相关知识,一方面是对上半年学习操作系统的总结,另一方面留着当作将来应对考试面试的复习笔记
进程
用通俗易懂的话来讲,进程就是处于执行期的程序。程序是一组计算机能识别的执行的指令集合,程序不是进程,执行中的程序才是进程。但进程可不仅仅局限于一段可执行程序代码,其通常还要包含其他资源,比如打开但文件、挂起的信号、数据段、处理器状态、一个具体内存映射的内存地址空间以及一个或者多个执行线程等等......实际上,进程就是正在执行的程序代码的实时结果。(线程是在进程中活动的对象,是linux内核调度的对象。linux系统对线程和进程并不特别区分,对linux而言线程就是一种特殊对进程。)
在现代操作系统中,进程提供两种虚拟机制:虚拟处理器和虚拟内存。虽然实际上许多进程在分享一个处理器,但虚拟处理器给进程一种假象,能让进程觉得自己在独享一个处理器;而虚拟内存则让进程在分配和管理内存时觉得自己拥有整个系统的所有内存资源。(在线程之间可以共享虚拟内存,但每个线程都拥有各自都虚拟处理器)
进程在创建的时刻开始存活,在linux中,这是通过调用fork()的结果。该函数通过复制一个现有的进程来创建一个新的进程。调用fork()都进程叫做父进程,新产生的进程叫做子进程。在该调用结束时,在返回的代码段的位置上,父进程恢复执行,子进程开始执行。fork()系统调用从内核返回两次:一次回到父进程中,另一次回到新的子进程.
//fork()实例 #include<unistd.h> #include<stdio.h> #include<stdlib.h> int main(int argc,char *argv[]){ pid_t pid=fork(); if ( pid < 0 ) { fprintf(stderr,"错误!"); } else if( pid == 0 ) { printf("子进程空间"); exit(0); } else { printf("父进程空间,子进程pid为%d",pid); } // 可以使用wait或waitpid函数等待子进程的结束并获取结束状态 exit(0); }
通常,新创建的进程都是为了立即执行新的、不同的程序,而接着调用exec()这组函数,这组函数能创建新的地址空间,并把新的程序载入其中。linux内核中,fork()实际是由clone()系统调用实现的(这里不讨论clone())。
最后,程序通过exit()系统调用进行退出。这个函数会终结进程并将其占用的资源全部释放掉。父进程可以通过wait()系统调用查询子进程是否终结,使得进程拥有等待特定进程执行完毕的能力。进程退出后被设为僵死状态(此时的进程称为僵尸进程),直到父进程调用wait()或者waitpid()为止(这个过程又叫收尸)
进程描述符以及任务结构(task_struct)
内核把进程的列表存放在叫做任务队列的双向循环列表中。链表中每一项都是类型为task_struct的结构,这个结构就是进程描述符。(学过操作系统就知道,为了描述控制进程的运行,系统中存放进程的管理和控制信息的数据结构称为程序控制块(PCB),每个进程都有一个PCB。在linux中这个PCB就是task_struct结构体)进程描述符包含一个具体进程的所有信息,进程描述符中包含的数据能完整的描述一个进程正在执行的程序:打开的文件、进程的空间地址、挂起的信号、进程的状态以及其他更多的信息。这里不作详细介绍,想深入学习可以参考这篇博文:https://blog.csdn.net/lf_2016/article/details/54347820
内核通过一个唯一的进程标示值或者PID来标示每个进程。PID是一个数,是一个int类型,PID默认的最大值为32768(可以通过修改文件来调整最大值),这个最大值实际上就是系统中允许同时存在的进程的最大数目。内核把每个PID存放在它们各自的进程描述符中。
进程描述符中通过state成员描述进程当前状态,系统中每个进程必然处于五种进程中的一种,该成员的值也必为五种状态之一:
TASK_RUNNING(运行)——进程是可执行的;它或者正在执行,或者在运行队列中等待执行。这是进程在用户空间执行的唯一可能状态。
TASK_INTERRUPTIBLE(可中断)——进程正在睡眠(也就是说进程被阻塞),等待某些条件的达成。一旦这些条件达成,内核就会把进程状态设为运行。处于此状态的进程也会因为接收到信号而提前被唤醒。
TASK_UNINTERRUPTIBLE(不可中断)——除了就算接收到信号也不会被唤醒外,这个状态与可中断状态相同。这个状态通常在进程必须在等待时不受干扰或等待事件很快发生时出现。
__TASK_TRACED ——被其他进程跟踪的进程,例如通过GDB对调试程序进行跟踪。
__TASK_STOPPED(停止)——进程停止执行。
进程创建
许多操作系统都提供了产生进程的机制,首先在新的地址空间里创建进程,读入可执行文件,最后开始执行。linux把上述步骤分解到两个单独的函数fork()和exec()中。首先fork()用过拷贝当前进程创建一个子进程,子进程与父进程的区别仅仅在于PID、PPID(父进程的进程号)和某些资源和统计量(例如挂起的信号)。exec()函数负责读取可执行文件并将其载入地址空间开始运行。
写时拷贝
传统的的fork()系统调用直接把所有资源复制给新创建的进程。这种实现过程过于简单且效率低下,因为它拷贝的数据或许并不共享,如果新进程打算立即执行一个新的映像,那么这些拷贝就是做无用功。linux 的 fork()改进了这一缺点,使用了写时拷贝页(copy-on-write)实现。写时拷贝是一种可以推迟甚至免除拷贝数据的技术。内核并不复制整个进程地址空间,而是让父、子进程共享同一个拷贝。
只有在需要写入的时候,数据才会被复制,从而使各个进程拥有各自的拷贝。也即是说,资源的复制只有在需要写入的时候才进行,在这之前,只是以只读的方式共享。这种技术使地址空间上的页的拷贝被推迟到实际发生写入的时候才进行。在页面不会被写入的情况下(如fork()之后立即调用exec()),它们就无需复制。
进程终结
当一个进程终结时,内核必须释放它所占有的资源并告知其父进程。它通常发生在进程调用exit()系统调用时,既可能显式调用这个系统调用,也可能隐式地从某个函数的主函数返回(c语言编译器会在main()函数返回点后面调用exit()代码)。调用完成后,进程相关联的所有资源都被释放掉。进程不可运行并处于EXIT_ZOMBIE退出状态。此时进程存在的唯一目的就是向他的父进程提供信息,父进程检索到信息后,由进程所持有的剩余内存被释放,归还给系统使用。(在父进程获得终止的进程信息前,子进程的task_struct是不会释放的)父进程调用wait()或者waitpid()回收子进程的信息