当前位置: 首页 > article >正文

【Linux0.11代码分析】06 之 kernel 初始化 init 进程代码分析

【Linux0.11代码分析】06 之 kernel 初始化 init 进程代码分析


系列文章如下:

系列文章汇总:《【Linux0.11代码分析】之 系列文章链接汇总(全)》
.
1.《【Linux0.11代码分析】01 之 代码目录分析》
2.《【Linux0.11代码分析】02 之 bootsect.s 启动流程》
3.《【Linux0.11代码分析】03 之 setup.s 启动流程》
4.《【Linux0.11代码分析】04 之 head.s 启动流程》
5.《【Linux0.11代码分析】05 之 kernel 初始化 init\main.c 代码分析》
6.《【Linux0.11代码分析】06 之 kernel 初始化 init 进程代码分析》



在前文《【Linux0.11代码分析】05 之 kernel 初始化 init\main.c 代码分析》中,
我们分析到,进程0main() 中 会调用 if( !fork() ){ init() ; } 来创建一个任务1子进程,
由任务1子进程来运行 init()函数,任务0父进程则作为空闲进程运行 for(;;) pause();

本文,我们重点分析下,进程1 运行 init() 的详细流程:

  1. 读取硬盘参数包括分区表信息,建立 RamDisk 虚拟盘,安装 RootFS 根文件系统设备
  2. 打开 tty0 字符设备 终端控制台, 问题来了: /dev/tty0 何时创建的?

代码如下:

// init\main.c
static char printbuf[1024];		// 用于内核显示信息的缓存

// 打印格式化后的字符串信息到输出设备 stdout(1)屏幕上显示,
// 该程序使用vsprintf()将格式化的字符串放入printbuf 缓冲区,然后用write()将缓冲区的内容输出到标准设备(1--stdout)
static int printf (const char *fmt, ...)
{
  	va_list args;			// char * args;
  	int i;
   	va_start (args, fmt);  // args=(char *) &(fmt) + ( (sizeof(fmt)+sizeof(int)-1) / sizeof (int) ) * sizeof(int)
    write (1, printbuf, i = vsprintf (printbuf, fmt, args));
    va_end (args);
    return i;
}

// 读取并执行/etc/rc文件时所使用的命令行参数和环境参数
static char *argv_rc[] ={"/bin/sh", NULL};		// 调用执行程序时参数的字符串数组。
static char *envp_rc[] ={"HOME=/", NULL};		// 调用执行程序时的环境字符串数组。

// 运行登录shell时所使用的命令行参数和环境参数
// 下面 argv[0]中的字符“-”是传递给shell程序sh的一个标志。
// 通过识别该标志,sh程序会作为登录shell执行。其执行过程与在shell提示符下执行sh不一样。
static char *argv[] ={"-/bin/sh", NULL};		// 同上。
static char *envp[] ={"HOME=/usr/root", NULL};

void init (void)
{
  	int pid, i;

	// 1. 读取硬盘参数包括分区表信息,建立 RamDisk 虚拟盘,安装 RootFS 根文件系统设备
    setup ((void *) &drive_info);		// linux-0.11\kernel\blk_drv\hd.c
    ===================>
    +// 读取CMOS 和硬盘参数表信息(位于0x90080),用于设置硬盘分区结构hd,并加载RAM 虚拟盘和 根文件系统, 
	+	int sys_setup(void * BIOS)
	+	{
	+		struct partition *p;
	+		struct buffer_head * bh;
	+		
	+		// 如果没有在config.h 中定义硬盘参数,就从0x90080 处读入
	+	#ifndef HD_TYPE
	+		for (drive=0 ; drive<2 ; drive++) {
	+			hd_info[drive].cyl = *(unsigned short *) BIOS;			// 柱面数
	+			hd_info[drive].head = *(unsigned char *) (2+BIOS);		// 磁头数
	+			hd_info[drive].wpcom = *(unsigned short *) (5+BIOS);	// 写前预补偿柱面号
	+			hd_info[drive].ctl = *(unsigned char *) (8+BIOS);		// 控制字节
	+			hd_info[drive].lzone = *(unsigned short *) (12+BIOS);	// 磁头着陆区柱面号
	+			hd_info[drive].sect = *(unsigned char *) (14+BIOS);		// 每磁道扇区数
	+			BIOS += 16;			// 每个硬盘的参数表长16 字节,这里BIOS 指向下一个表
	+		}
	+		// setup.s 程序在取BIOS 中的硬盘参数表信息时,如果只有1 个硬盘,就会将对应第2 个硬盘的16 字节全部清零。
	+		// 因此这里只要判断第2 个硬盘柱面数是否为0 就可以知道有没有第2 个硬盘了
	+		if (hd_info[1].cyl)		NR_HD=2;			// 硬盘数置为2
	+		else					NR_HD=1;
	+	#endif
	+		// 设置每个硬盘的起始扇区号和扇区总数
	+		for (i=0 ; i<NR_HD ; i++) {
	+			hd[i*5].start_sect = 0;			// 起始扇区号
	+			hd[i*5].nr_sects = hd_info[i].head*hd_info[i].sect*hd_info[i].cyl;		// 扇区总数
	+		}
	+		// 可能会出现这样的情况,我们有一块SCSI/ESDI/等的控制器,它是以ST-506 方式与BIOS 兼容的,
	+		// 因而会出现在我们的BIOS 参数表中,但却又不是寄存器兼容的,因此这些参数在CMOS 中又不存在,
	+		// 我们假设ST-506 驱动器(如果有的话)是系统中的基本驱动器,也即以驱动器1 或2出现的驱动器
	+		// 第1 个驱动器参数存放在CMOS 字节0x12 的高半字节中,第2 个存放在低半字节中。
	+		// 该4 位字节息可以是驱动器类型,也可能仅是0xf
	+		// 0xf 表示使用CMOS 中0x19 字节作为驱动器1 的8 位类型字节,使用CMOS 中0x1A 字节作为驱动器2 的类型字节。
	+		// 总之,一个非零值意味着我们有一个AT 控制器硬盘兼容的驱动器
	+		if ((cmos_disks = CMOS_READ(0x12)) & 0xf0)
	+			if (cmos_disks & 0x0f)		NR_HD = 2;
	+			else						NR_HD = 1;
	+		else	NR_HD = 0;
	+		
	+		// 若NR_HD=0,则两个硬盘都不是AT 控制器兼容的,硬盘数据结构清零。
	+		// 若NR_HD=1,则将第2 个硬盘的参数清零。			
	+		for (i = NR_HD ; i < 2 ; i++) {
	+			hd[i*5].start_sect = 0;		// 起始扇区号
	+			hd[i*5].nr_sects = 0;		// 扇区总数
	+		}
	+		for (drive=0 ; drive<NR_HD ; drive++) {
	+			// 读取每一个硬盘上第1 块数据(第1 个扇区有用),获取其中的分区表信息, 参数中的0x300 是硬盘的主设备号
	+			if (!(bh = bread(0x300 + drive*5,0))) {
	+				printk("Unable to read partition table of drive %d\n\r",drive);
	+				panic("");
	+			}
	+			//然后根据硬盘头1个扇区位置0x1fe处的两个字节是否为'55AA'来判断该扇区中位于0x1BE开始的分区表是否有效
	+			if (bh->b_data[510] != 0x55 || (unsigned char) bh->b_data[511] != 0xAA) {
	+				printk("Bad partition table on drive %d\n\r",drive);
	+				panic("");
	+			}
	+			// 最后将分区表信息放入硬盘分区数据结构hd 中
	+			p = 0x1BE + (void *)bh->b_data;
	+			for (i=1;i<5;i++,p++) {
	+				hd[i+5*drive].start_sect = p->start_sect;		// 起始扇区号
	+				hd[i+5*drive].nr_sects = p->nr_sects;			// 扇区总数
	+			}
	+			brelse(bh);		// 释放为存放硬盘块而申请的内存缓冲区页
	+		}
	+		if (NR_HD)			// 如果有硬盘存在并且已读入分区表,则打印分区表正常信息
	+			printk("Partition table%s ok.\n\r",(NR_HD>1)?"s":"");
	+		// 加载(创建)RAMDISK(kernel/blk_drv/ramdisk.c,71)。
	+		rd_load();
	+		// 安装根文件系统(fs/super.c,242)
	+		mount_root();
	+		return (0);
	+	}
    <===================
	
	// 2. 打开 tty0 字符设备 终端控制台, 问题来了: /dev/tty0 何时创建的?
    (void) open ("/dev/tty0", O_RDWR, 0);	// 用读写访问方式打开设备“/dev/tty0”, 返回的句柄号0 -- stdin 标准输入设备。
    // 复制0号句柄,产生句柄1号,用作stdout标准输出设备
    // 一个程序支持句柄最大为20个,sys_dup会使用一个当前可用的最小文件句柄,
    // 如 /dev/tty0已经打开为0句柄,下面可用的最小句柄为1,此处复制后的句柄为
    (void) dup (0);		
    ====================>
	+	// 复制指定文件句柄oldfd,新句柄的值是当前最小的未用句柄。
	+	int sys_dup (unsigned int fildes){
	+	  return dupfd (fildes, 0);
	+	}
    <===================
    // 复制0句柄,产生句柄2,用作stderr 标准出错输出设备。
    (void) dup (0);		
    // 打印缓冲区块数和总字节数,每块1024 字节
    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);	
    
    // 使用fork()创建一个子进程,
  	// 下面fork()用于创建一个子进程(子任务)。对于被创建的子进程,fork()将返回0 值,
  // 对于原(父进程)将返回子进程的进程号。所以180-184 句是子进程执行的内容。该子进程
  // 关闭了句柄0(stdin),以只读方式打开/etc/rc 文件,并执行/bin/sh 程序,所带参数和
  // 环境变量分别由argv_rc 和envp_rc 数组给出。参见后面的描述。
  if (!(pid = fork ()))
    {
      close (0);
      if (open ("/etc/rc", O_RDONLY, 0))
	_exit (1);		// 如果打开文件失败,则退出(/lib/_exit.c,10)。
      execve ("/bin/sh", argv_rc, envp_rc);	// 装入/bin/sh 程序并执行。
      _exit (2);		// 若execve()执行失败则退出(出错码2,“文件或目录不存在”)。
    }
  // 下面是父进程执行的语句。wait()是等待子进程停止或终止,其返回值应是子进程的进程号(pid)。
  // 这三句的作用是父进程等待子进程的结束。&i 是存放返回状态信息的位置。如果wait()返回值不
  // 等于子进程号,则继续等待。
  if (pid > 0)
    while (pid != wait (&i))
      /* nothing */ ;
  // 如果执行到这里,说明刚创建的子进程的执行已停止或终止了。下面循环中首先再创建一个子进程,
  // 如果出错,则显示“初始化程序创建子进程失败”的信息并继续执行。对于所创建的子进程关闭所有
  // 以前还遗留的句柄(stdin, stdout, stderr),新创建一个会话并设置进程组号,然后重新打开
  // /dev/tty0 作为stdin,并复制成stdout 和stderr。再次执行系统解释程序/bin/sh。但这次执行所
  // 选用的参数和环境数组另选了一套(见上面165-167 行)。然后父进程再次运行wait()等待。如果
  // 子进程又停止了执行,则在标准输出上显示出错信息“子进程pid 停止了运行,返回码是i”,然后
  // 继续重试下去…,形成“大”死循环。
  while (1)
    {
      if ((pid = fork ()) < 0)
	{
	  printf ("Fork failed in init\r\n");
	  continue;
	}
      if (!pid)
	{
	  close (0);
	  close (1);
	  close (2);
	  setsid ();
	  (void) open ("/dev/tty0", O_RDWR, 0);
	  (void) dup (0);
	  (void) dup (0);
	  _exit (execve ("/bin/sh", argv, envp));
	}
      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() */
}






待研究:

  1. rd_load () // 加载(创建)RAMDISK
  2. mount_root (); // 安装根文件系统
  3. /dev/tty0 终端设备(tty)驱动架构
    《linux终端设备:tty子系统相关的初始化》
    《Linux的tty设备介绍》

http://www.kler.cn/news/18105.html

相关文章:

  • 如何用ChatGPT做一门课?(包含大纲、脚本、PPT文本)
  • Android---启动速度优化
  • 了解MSIL汇编和IL汇编评估堆栈
  • 【思科、华为、华三、锐捷网络设备巡检命令】
  • PAT A1035 Password
  • 机器人控制系统学习和研究中数学的重要性
  • 数据库系列-什么是 JDBC?它的作用是什么?
  • centos7 安装python的命令
  • 【Halcon】找到设备上的 标识牌
  • Java设计模式(十八)中介者模式
  • Nacos注册中心一些配置说明
  • 《Netty》从零开始学netty源码(五十三)之PoolThreadCache的功能
  • MySQL面试八股文:索引篇
  • 我把Solon打包成了native image,速度快的惊人
  • 【linux的学习与软件安装】
  • 计算机操作系统实验:页面置换算法的实现
  • 充电桩测试设备TK4800充电桩现校仪检定装置
  • MySQL优化二索引使用
  • 信息安全从业人员职业规划(甲方乙方分别说明)
  • 中兴B860AV2.1-T(M)-高安版-当贝纯净桌面线刷固件包
  • Facebook 用户量十分庞大,为什么还使用 MySQL 数据库?
  • IDEA沉浸式编程体验
  • 锁相环技术,单边带信号,信号的调制
  • MySQL数据库之索引
  • 【SpringMVC】三、SpringMVC获取请求参数与域数据共享
  • Ubuntu20.04安装Vtk9.2.6+PCL1.12.1(成功无报错)
  • 使用asp.net core web api创建web后台,并连接和使用Sql Server数据库
  • Flink dataStream,如何开窗,如何进行窗口内计算
  • BM54-三数之和
  • 盲目自学网络安全只会成为脚本小子?