操作系统学习:进程、线程与Linux0.12初始化过程概述

本文参考书籍

1.操作系统真相还原
2.Linux内核完全剖析:基于0.12内核
3.x86汇编语言  从实模式到保护模式
ps:基于x86硬件的pc系统

进程

进程是一种控制流集合,集合中至少包含一条执行流,执行流之间就是相互独立的,但它们共享进程的所有资源,它们是处理器的执行单位,或者称为调度单位,它们就是线程。
每个进程都运行在自己的地址空间中,有内存空间才能存储资源,因此进程拥有此程序运行所需的全部资源,默认情况下进程中只有一个执行流,即一个进程只能干一件事,有些情况下,需要在一个地址空间中存在多个执行流,即让进程同时并行做很多事,这多个执行流就是线程,执行流就是调度器的基本单位,是处理器的执行单元,线程在此方面和进程的行为是一致的,只不过线程不包括位于进程中的、自己所需要的资源,言外之意是线程没有自己独享的地址空间,没空间就无法存储自己的资源,所以线程必须借助进程空间中的资源运行。因此,线程和进程比,进程拥有整个地址空间、从而拥有全部资源,线程没有自己的地址空间,因此没有任何属于自己的资源,需要借助进程的资源生存,所以线程被称为轻量级进程。进程和线程都是执行流,它们都具备独立寄存器资源和独立的栈空间,因此线程页可以像进程那样调用其他函数。
在显式创建了线程之后,任务调度器就可以把它对应的代码块从进程中分离出来单独调度上处理器执行了,否则调度器会把整个进程当成一个大的执行流,也可以说是把整个进程当成一个线程,从头到尾依次执行下去。

进程和线程同样是执行流,最初进程中只有一条执行流,程序就应该沿着这条路执行下去,后来为了让程序提速,进程中的执行流变成了两条以上,为了强调进程中包含不同的程序流(执行流),这才出现了线程的概念,其实在处理器上运行的执行流都是人为划分的逻辑上独立的程序段,本质上是一段代码区域,只不过线程是纯粹的执行部分,它运行所需要的资源存储在进程这个大房子中,进程中包含此进程中所有线程使用的资源,因此线程依赖于进程,存在于进程之中,用表达式表达:进程=线程+资源。

进程拥有整个地址空间,其中包括各种资源,而进程中的所有线程共享同一个地址空间,因为这个地址空间中有线程运行所需要的资源。
即使进程中未显式创建线程的话,进程中总会有一个向下执行的方向,即执行流,即单线程进程,如果进程中显示创建了多个线程的话,此进程称为多线程进程,总之线程属于进程之内,进程内必有线程,任何进程有自己的执行流,如果只有一个执行流,该执行流可以称为主线程,其他新的线程也要通过此主线程创建。

线程是具有能动性、执行力、独立的代码块,进程是由线程+资源组成,根据进程内线程的数量,可分为单线程进程即没有显式创建线程的进程、多线程进程即显式创建线程的程序,执行流、调度单位、运行实体等概念都是针对线程而言,线程才是解决问题的思路、步骤,它是具有能动性的指令,因此只有他才能上处理器运行,即一切执行流其实都是线程,因为任何时候进程中都至少存在一个线程。进程独自拥有整个地址空间,在这个空间中装有线程运行所需的资源,所以地址空间相当于资源容器,进程与线程的关系是进程是资源容器,线程是资源使用者,进程与线程的区别是线程没有自己独享的资源,因此没有自己的地址空间,它要依附于进程的地址空间中借助进程的资源运行。

现代操作系统都是多任务操作系统,每个任务都要被调度到处理器上分时运行,运行一段时间后再被换下来,由调度系统根据调度算法再选下一个线程上处理器。操作系统为每个进程提供了一个PCB,即程序控制块,它就是进程的身份证,用它来记录与此进程相关的信息,比如进程状态、PID、优先级等。一般PCB的结构如图所示;
PCB结构
每个进程都有自己的PCB,所有PCB放到一张表格中维护,这就是进程表,调度器可以根据这张表选择上处理器运行的进程,如图所示;
进程表

线程的实现方式

线程的实现就有两种方式,要么由操作系统原生支持,用户进程通过系统调用使用线程,要么操作系统不支持线程,由进程自己创建管理。因此,线程要么在0特权级的内核空间中实现,要么在3特权级的用户空间实现。在0特权级的内核空间实现,只是说线程机制由内核来提供,并不是线程中所运行的代码也必须是0特权级的内核级代码,也可以是3特权级的用户级代码,而在3特权级的用户空间实现,是指线程机制由用户进程自己提供,相当于用户进程除了负责业务外,还要在进程中实现线程调度器,这样一来程序员负担比较重,所以通常情况下很少有程序员愿意在进程中写线程机制,故标准库便提供了用户线程库。总之,无论线程机制是由内核,还是用户进程提供,都是为用户进程服务的,线程中必须可以运行用户的代码。

线程仅是执行流,在用户空间,还是在内核空间实现它,最大的区别就是线程表在哪里,由谁来调度它上处理器,如果线程在用户空间中实现,线程表就在用户进程中,用户进程就要专门写个线程用作线程调度器,由它来调度进程内部的其他线程。如果线程在内核空间中实现,线程表就在内核中,该线程就会由操作系统的调度器统一调度,无论该线程属于内核,还是用户进程。

用户线程

在用户空间中实现线程的好处是可移植性强,由于是用户级的实现,所以在不支持线程的操作系统上也可以写出完美支持线程的用户程序,在用户空间中实现线程,操作系统根本就不会意识到线程的存在,因为操作系统调度器只会以整个进程的方式调度,将处理器的适用权交给这个进程,由进程中的调度器自己去协调分配处理器时间。无论线程在哪里实现,目的都是要到处理器上运行,因此必然要考虑到线程调度的问题,这涉及到调度器及线程表。用户进程中,一般是某个权威机构发布个用户级线程包,开发人员在用户进程中调用此包中的方法去创建线程、结束线程等,线程包中一定存在着线程调度器,而且,线程包中的方法都会与此线程调度器有调用关系,这样当有新线程产生或有线程退出时,线程调度器才会被调用,从而在内部维护的线程表中找出下一个线程上处理器运行。

在用户进程中实现线程有如下优点:
1.线程的调度算法由用户程序自己实现,可以根据实现应用情况为某些线程调用;
2.将线程的寄存器映像装载到CPU时,可以在用户空间完成,即不用陷入内核态,这样就免去了进入内核时的入栈及出栈操作。
相应的缺点如下:
1.进程中的某个线程出现阻塞,整个进程会被挂起,即进程中的全部线程都无法运行;
2.线程未在内核空间中实现,因此对于操作系统来说,调度器的调度单元是整个进程,并不是进程中的线程,所以时钟中断只能影响进程一级的执行流,如果在进程中的某个线程如果开始在处理器上执行时,只要该线程不主动让出处理器,此进程中的其他进程都没机会运行,也就是说,没有保险的机制使线程运行适时,即避免单一线程过度使用处理器,而其他线程没有调度的机会。
3.线程在用户空间实现,和在内核空间实现相比,只是在内部调度时少了陷入内核的代价,确实相当于提速,但由于整个进程占据处理器的时间片是有限的,这有限的时间片还要再分给内部的线程,所以每个线程执行的时间片非常短暂,再加上进程内线程调度器维护线程表,运行调度算法的时间片消耗,反而抵消了内部调度带来的提速。

内核线程

内核线程是由内核提供原生线程机制,用户进程中不再单独实现。
线程由内核实现的优点如下:
1.相比在用户空间中实现线程,内核提供的线程相当于让进程多占了处理器资源,比如系统中运行有进程A和一传统型进程B,此时进程A中显式的创建了3个线程,进程A加上主线程便有了4个线程,加上进程B,内核调度器便有了5个独立的执行流,尽管其中4个都属于进程A,但对调度器来说4个线程和进程一样被调度,因此调度器调度完,进程A使用了80%的处理器资源(理想状态),这就达到了提速;
2.当进程中某一线程阻塞后,由于线程是由内核空间实现的,操作系统就只会阻塞这一个线程,此线程所在进程内的其他线程将不受影响。
缺点如下:
1.用户进程需要通过系统调用陷入内核,这增加了一些现场保护的栈曹组,还是会消耗一些处理器资源。

如图所示,表明两种实现方式的不同:
线程的两种实现方式

扫描二维码关注公众号,回复: 1572523 查看本文章

Linux初始化过程概述

在Linux启动执行到main函数后,main程序利用已经在setup,S中所取得的系统参数设置系统的跟文件设备以及一些内存全局变量,这些内存变量指明了主内存的开始地址、系统所拥有的内存容量和作为高速缓冲区内存的某段地址,如果还定义了虚拟盘,则将主内存将适当减少,高速缓冲区是用于磁盘等块设备临时存放数据的地方,主内存区域的内存由内存管理模块mm通过分页机制进行管理分配,以4KB字节为一个内存页单位,内核程序可以自由访问高速缓冲中的数据,但需要通过mm才能使用分配到的内存页面。然后,内核进行硬件初始化工作,包括陷阱门、块设备、字符设备和tty,还包括人工设置第一个任务task0,待所有初始化工作完成后程序就设置中断允许标志以开启中断,并切换到任务0中运行,在整个内核初始化完成后,内核将执行权切换到用户模式,即CPU从0特权级切换到3特权级,此时main的主程序就工作在任务0中,然后系统第一次调用进程创建函数fork(),创建出一个用于运行init()的子进程,整个初始化过程如图所示;
内核初始化过程

main程序首先确定如何分配使用系统物理内存,然后调用内核各部分的初始化函数分别对内存管理、中断处理、块设备和字符设备、进程管理以及硬盘进行初始化处理。在完成了这些操作之后,系统各部分已经处于可运行状态,此后程序把自己移动到任务0中运行,并使用fork()调用使用首次创建出进程1(init进程),并在其中调用init()函数,在该函数中程序将继续进行应用环境的初始化并执行shell程序,而原进程0则会在系统空闲时被调度执行,因此进程0通常也被称为idle程序,此时进程0仅执行pause()系统调用,并又会调用调度函数。
init函数的功能可分为4个部分:安装根文件系统、显示系统信息、运行系统初始资源配置文件rc中的命令、执行shell程序。
首先调用系统调用setup(),用来收集硬盘设备分区表信息并安装根文件系统,在安装根文件系统之前,系统会先判断是否需要建立虚拟盘,若编译内核时设置了虚拟盘的大小,并在前面内核初始化过程中已经开辟了一块内存用作虚拟盘,则内核就会首先尝试把根文件系统加载到内存的虚拟盘区中,然后init()打开一个中断设备tty0,并复制其文件描述符以产生标准输入stdin、标准输出stdout和错误输出stderr设备,内核随后利用这些描述符在终端上显示一些系统信息,接着init()又新建了一个进程(进程2),并在其中为建立用户交互使用环境而执行一些初始配置操作,即在用户可以使用shell命令行环境之前,内核调用/bin/sh程序运行了配置文件/etc/rc中设置的命令,这段代码首先通过关闭文件描述符0,并立刻打开文件/etc/rc,从而把标准输入stdin定向到/etc/rc文件上,所有的标准输入数据都将从该文件中读取,然后内核以非交互形式执行/bin/sh,从而执行/etc/rc文件中的命令,当该文件中的命令执行完成后,/bin/sh就会立刻退出,因此进程2随之结束。init()函数的最后一部分用于在新建进程中为用户建立一个新的会话,并允许shell,在系统执行进程2中的程序时,父进程一直等待着它的结束,随着进程2的退出,父进程就进入一个无线循环中,在该循环中,父进程会再次生成一个新进程,然后在该进程中创建一个新的会话,并再吃执行/bin/sh,以创建用户交互shell环境,随着父进程继续等待该子进程。虽然shell与前面的非交互式shell是同一程序/bin/sh,但是所适用的命令行参数不同,shell的第0个命令行参数的第1个字符带有’-‘号,这个特定的标志会在/bin/sh执行时通知它不是一次普通的运行,而是作为登录shell运行/bin/sh,从此时开始,用户就可以正常适用Linux命令行环境了。

以上过程也可以概括为如下三个阶段;
第一阶段:创建进程0,并让进程0具备在32位保护模式下载主机中运算的能力。每一个进程都是由一个已经存在的进程创建的,这个新建的进程将具备创建它的进程所具备的一切能力,因此,系统先创建一个原始的进程(进程0),并使之具备进程应该具备的最基本的能力,在主机中进行运算。
第二阶段:以进程0为母本创建进程1,使进程1不仅仅具备进程0所拥有的能力,而且还能以文件的形式与外设进行数据交互。需要在进程0已经具备的运算能力的基础上进一步拓展以文件的形式与外设进行数据交互的环境。
第三阶段:以进程1为母本创建进程2,使进程2在全面具备进程1所拥有的能力和环境的基础上,进一步具备支持人机交互的能力。

static inline _syscall0(int,fork)                   // 无参的系统调用fork,生成新的进程
static inline _syscall0(int,pause)                  // 无参的系统调用pause,当调用到时就调度
static inline _syscall1(int,setup,void *,BIOS)      // 带一个参数的系统调用 setup 磁盘初始化的系统调用
static inline _syscall0(int,sync)                   // 更新文件系统,同步数据

#include <linux/tty.h>
#include <linux/sched.h>
#include <linux/head.h>
#include <asm/system.h>
#include <asm/io.h>

#include <stddef.h>
#include <stdarg.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>

#include <linux/fs.h>

#include <string.h>

static char printbuf[1024];

extern char *strcpy();
extern int vsprintf();
extern void init(void);
extern void blk_dev_init(void);
extern void chr_dev_init(void);
extern void hd_init(void);
extern void floppy_init(void);
extern void mem_init(long start, long end);
extern long rd_init(long mem_start, int length);
extern long kernel_mktime(struct tm * tm);

static int sprintf(char * str, const char *fmt, ...)        // 内核专用打印函数
{
    va_list args;
    int i;

    va_start(args, fmt);
    i = vsprintf(str, fmt, args);
    va_end(args);
    return i;
}

/*
 * This is set up by the setup-routine at boot-time
 */
#define EXT_MEM_K (*(unsigned short *)0x90002)                  // 扩展内存大小
#define CON_ROWS ((*(unsigned short *)0x9000e) & 0xff)
#define CON_COLS (((*(unsigned short *)0x9000e) & 0xff00) >> 8)
#define DRIVE_INFO (*(struct drive_info *)0x90080)              // 硬盘的参数信息
#define ORIG_ROOT_DEV (*(unsigned short *)0x901FC)              // 根文件系统所在设备号
#define ORIG_SWAP_DEV (*(unsigned short *)0x901FA)              // 交换文件所在设备号

/*
 * Yeah, yeah, it's ugly, but I cannot find how to do this correctly
 * and this seems to work. I anybody has more info on the real-time
 * clock I'd be interested. Most of this was trial and error, and some
 * bios-listing reading. Urghh.
 */

#define CMOS_READ(addr) ({ \
outb_p(0x80|addr,0x70); \
inb_p(0x71); \
})                // 读取cmos实时时钟信息

#define BCD_TO_BIN(val) ((val)=((val)&15) + ((val)>>4)*10)

static void time_init(void)                 // 读取CMOS实时时钟信息作为开机时间,并保持到全局变量中
{
    struct tm time;

    do {
        time.tm_sec = CMOS_READ(0);
        time.tm_min = CMOS_READ(2);
        time.tm_hour = CMOS_READ(4);
        time.tm_mday = CMOS_READ(7);
        time.tm_mon = CMOS_READ(8);
        time.tm_year = CMOS_READ(9);
    } while (time.tm_sec != CMOS_READ(0));
    BCD_TO_BIN(time.tm_sec);
    BCD_TO_BIN(time.tm_min);
    BCD_TO_BIN(time.tm_hour);
    BCD_TO_BIN(time.tm_mday);
    BCD_TO_BIN(time.tm_mon);
    BCD_TO_BIN(time.tm_year);
    time.tm_mon--;
    startup_time = kernel_mktime(&time);
}

static long memory_end = 0;                     // 机器物理内存容量
static long buffer_memory_end = 0;              // 高速缓冲区末端地址
static long main_memory_start = 0;              // 主内存的开始位置
static char term[32];                           

static char * argv_rc[] = { "/bin/sh", NULL };       // /etc/rc文件时使用的命令行参数和环境变量
static char * envp_rc[] = { "HOME=/", NULL ,NULL };

static char * argv[] = { "-/bin/sh",NULL };          // 运行登录shell时的参数和环境变量 - 作为登录shell的标志传入
static char * envp[] = { "HOME=/usr/root", NULL, NULL };

struct drive_info { char dummy[32]; } drive_info;    // 存放硬盘信息的结构

void main(void)     /* This really IS void, no error here. */
{           /* The startup routine assumes (well, ...) this */
/*
 * Interrupts are still disabled. Do necessary setups, then
 * enable them
 */
    ROOT_DEV = ORIG_ROOT_DEV;                           // 根文件系统设备号
    SWAP_DEV = ORIG_SWAP_DEV;                           // 交换文件设备号
    sprintf(term, "TERM=con%dx%d", CON_COLS, CON_ROWS);  
    envp[1] = term; 
    envp_rc[1] = term;
    drive_info = DRIVE_INFO;                            // 赋值内存0x90080处的硬盘参数
    memory_end = (1<<20) + (EXT_MEM_K<<10);             // 内存大小1MB+扩展内存(k)*1024
    memory_end &= 0xfffff000;                           // 忽略不到4KB的内存数
    if (memory_end > 16*1024*1024)                      // 如果内存量超过16MB,按照16MB来
        memory_end = 16*1024*1024;
    if (memory_end > 12*1024*1024)                      // 如果内存大于12MB,则设置缓冲区末端4MB
        buffer_memory_end = 4*1024*1024;
    else if (memory_end > 6*1024*1024)                  // 如果内存大于6MB,则设置缓冲区大小2MB
        buffer_memory_end = 2*1024*1024;
    else
        buffer_memory_end = 1*1024*1024;                // 否则设置缓冲区末端为1MB
    main_memory_start = buffer_memory_end;              // 主内存起始位置等于缓冲区末端
#ifdef RAMDISK                                          // 如果编译时定义了内存虚拟盘则初始化虚拟盘
    main_memory_start += rd_init(main_memory_start, RAMDISK*1024);  // 减少了主内存的量
#endif
    mem_init(main_memory_start,memory_end);             // 主内存初始化
    trap_init();                                        // 陷阱门初始化
    blk_dev_init();                                     // 块设备初始化
    chr_dev_init();                                     // 字符设备初始化
    tty_init();                                         // tty初始化
    time_init();                                        // 设置开机时间
    sched_init();                                       // 调度程序初始化
    buffer_init(buffer_memory_end);                     // 缓冲管理初始化
    hd_init();                                          // 硬盘初始化
    floppy_init();                                      // 软驱初始化
    sti();                                              // 开启中断
    move_to_user_mode();                                // 移到用户态执行
    if (!fork()) {      /* we count on this going ok */  // 在新建的子进程中执行init()函数
        init();
    }
/*
 *   NOTE!!   For any other task 'pause()' would mean we have to get a
 * signal to awaken, but task0 is the sole exception (see 'schedule()')
 * as task 0 gets activated at every idle moment (when no other tasks
 * can run). For task0 'pause()' just means we go check if some other
 * task can run, and if not we return here.
 */
    for(;;)
        __asm__("int $0x80"::"a" (__NR_pause):"ax");   // 任务0倍调度时执行系统调用pause 
}

static int printf(const char *fmt, ...)                 // 产生标准输出
{
    va_list args;
    int i;

    va_start(args, fmt);
    write(1,printbuf,i=vsprintf(printbuf, fmt, args));
    va_end(args);
    return i;
}

void init(void)
{
    int pid,i;

    setup((void *) &drive_info);                // 系统调用读取硬盘信息并加载到虚拟盘
    (void) open("/dev/tty1",O_RDWR,0);          // 以读写方式打开tty
    (void) dup(0);                              // 复制句柄,产生句柄1号标准输出设备
    (void) dup(0);                              // 复制句柄,产生句柄2号标准错误输出
    printf("%d buffers = %d bytes buffer space\n\r",NR_BUFFERS,
        NR_BUFFERS*BLOCK_SIZE);                 // 打印内存信息
    printf("Free mem: %d bytes\n\r",memory_end-main_memory_start);
    if (!(pid=fork())) {                        // 执行/etc/rc中的命令参数 
        close(0);
        if (open("/etc/rc",O_RDONLY,0))
            _exit(1);                           // 若文件打开失败则立刻退出
        execve("/bin/sh",argv_rc,envp_rc);      // 系统调用执行命令
        _exit(2);                               // 若执行出错则退出
    }
    if (pid>0)
        while (pid != wait(&i))                 // 父进程等待子进程执行完成
            /* nothing */;
    while (1) {
        if ((pid=fork())<0) {
            printf("Fork failed in init\r\n");
            continue;                           // 如果fork出错则继续fork
        }
        if (!pid) {                             // 新的子进程
            close(0);close(1);close(2);         
            setsid();                           // 创建一组会话
            (void) open("/dev/tty1",O_RDWR,0);  // 以读写的方式打开终端
            (void) dup(0);                      
            (void) dup(0);
            _exit(execve("/bin/sh",argv,envp)); // 执行shell程序
        }
        while (1)
            if (pid == wait(&i))                // 如果子进程退出则继续循环
                break;                          // 停止循环
        printf("\n\rchild %d died with code %04x\n\r",pid,i);
        sync();                                 // 同步操作,刷新缓冲区
    }
    _exit(0);   /* NOTE! _exit, not exit() */
}

以上代码,大致描述了初始化的过程,详细的各个部分的初始化后续会详细分析。

猜你喜欢

转载自blog.csdn.net/qq_33339479/article/details/80643722