Toyix简明教程

              版权声明:本教程版权归www.toyix.tech所有。

欢迎使用Toyix!Toyix是一个专门为操作系统基础理论教学而编写的系统。本教程将介绍Toyix 1.00的安装和使用。


 

目录

第1章 Toyix的安装及基本使用

1.1 Toyix的安装

1.2 Toyix的启动

1.3 Toyix的退出

1.4 Toyix下的第一个程序

第2章 Toyix编程

2.1 进程模型

2.2 进程的创建

2.3 进程间通信

 


第1章 Toyix的安装及基本使用

要使用一个操作系统,必须要先安装这个系统,然后再熟悉它的常用功能的使用。


1.1 Toyix的安装


安装步骤:

1、下载TOYIXCE.zip文件,将压缩文件中的TOYIXCE文件夹解压到D盘根目录下,如下图。

2、打开TOYIXCE文件夹,浏览Toyix的系统文件,查看Toyix系统文件的目录结构,如下图。

如果看到toyix的系统文件在D:\TOYIXCE目录下,说明Toyix系统已安装完毕。


 

1.2 Toyix的启动

Toyix需要在dos系统下启动,所以要先进入dos系统,进入dos系统的方法有如下几种:

1、在系统启动时进入实模式下的dos。

计算机装的操作系统不同进入实模式dos的方式也有所不同。如果只装了dos系统,那计算机启动后就直接进入实模式dos;如果是windows 98系统,在系统启动时可以按住F8,选择进入实模式dos;如果是windows 2000以上的系统无法直接进入实模式dos,如果要进入实模式dos,可以安装dos工具箱(如:矮人dos工具、Max dos工具箱等),这样在启动时会出现启动菜单,在启动菜单上选择进入工具箱里面的dos即可启动实模式dos。

2、在windows下进入虚拟8086模式下的dos。

首先点击“开始”菜单,如下图。

然后选择“运行”菜单 ,并在文本框中输入“command”命令,如下图。

点击“确定”按钮,进入虚拟8086 模式dos,如下图。

进入dos后,需要运行Toyix系统目录下的“co.bat ”来启动Toyix系统,首先把当前目录切换到D:\TOYIXCE,如下图。

然后输入命令“co”,按回车键,就启动Toyix了。Toyix启动后的界面如下图

看到此界面时你已经成功启动了Toyix系统。提示符上面显示了Toyix的版本及开发者姓名等信息。


 

1.3 Toyix的退出

输入“down”命令,按回车键,就退出Toyix了,toyix退出后系统回到Dos下,如下图。

Toyix退出前后明显的不同是提示符信息的变化,启动toyix时提示符前有“{Co.}”,退出后“{Co.}”消失(注意“{Co.}”只是为了区分dos和toyix的命令行,完全可以显示其它信息或不显示)。


 

1.4 Toyix下的第一个程序


         一个程序从开始编写源代码到执行一般要经过下面几个步骤:编辑源代码、编译、连接、执行,下面看一下在toyix下是如何完成这个过程的。

1、编辑源程序

可以用任何文本编辑器编辑源程序,最后只要保存成纯文本文件即可。在toyix下自带了一个文本编辑器edit,启动过程很简单,只要输入“edit”命令,按回车键,这个编辑器就启动了,如下图。


Edit启动后的界面如下图:

在Edit中输入程序代码:

#include <toyix.h>
main()
{
        printf("\nHello World!\n");
}

保存为文件c:\1.c后,退出edit,结束对源代码的编辑。

2、编译源程序

Toyix的编译命令为“cc”,命令 格式为“cc 文件名”,编译1.c这个源文件就输入“ cc 1.c”,回车,就编译了,如下图。

如果没有编译和连接错误的话,就生成文件1.prg, prg文件是toyix系统的可执行文件。

3、运行可执行文件

运行可执行文件的命令是“do”,命令格式“do 可执行文件名”。先运行一下“1.prg”这个可执行文件,如下图。

回车执行“do”命令后,如下图。

提示按任意键将可执行文件1.prg。执行这个程序前,先介绍一下toyix的进程监视器,上面的蓝条是toyix的进程监视器显示部分,它显示了所有进程的状态,“running”后面显示的是正在执行进程的进程号,“ready”后面显示的是就绪进程的进程号,“blocked”后面显示的是阻塞进程的进程号。执行“do 1.prg”后,这个程序的进程处于就绪态,“ready”后显示这个就绪进程的进程号,只需按任意键,就可以执行这个进程了。1.prg的运行结果如下图:

这样一个简单的输出“Hello World!”的程序就执行完毕了,要完成功能更多的程序只需在源代码中写入更多的功能代码即可,步骤与1.c这个程序的步骤相同。

上面介绍了如何运行一个程序,但Toyix是一个多任务操作系统,如何执行多个任务呢? 就像在windows里我们可以同时运行两个记事本, 在toyix如何同时运行两个任务呢?还是用命令“do”,命令格式变为“do 文件1 文件2 文件3 …….”,执行三个1.prg,就输入“do 1 1 1”,如下图。

执行结果如下图:

输出了三次“Hello World!”,启动了三个“1.prg”,每个都被执行了。这样我们就知道了如何运行一个或多个程序了。


到此toyix的安装及基本使用部分就完了,你会安装toyix和使用它的基本功能了吗?


 

第2章 Toyix编程


Toyix编程部分主要讲解toyix系统提供的库函数。Toyix库函数主要分两类:一类是与系统相关的库函数,这是介绍的重点;另一类是c库函数,和一般c库函数的使用基本相同,就不再介绍,读者使用时可以查阅函数手册

操作系统中最重要的是进程,进程主要涉及到下面几个操作:

  1. 得到进程的标识进程号PID 。
  2. 创建新的进程 。
  3. 进程间的通信(与信号量相关的同步和互斥也是通信的一种) 。

下面我们将逐步用编程使用的方式介绍与这几个操作相关的系统函数。

2.1 进程模型


在介绍进程模型前,要先了解一下进程,什么是进程呢?进程是程序在计算机上的一次执行活动,当运行一个程序,就启动了一个进程。程序运行活动可以用一个进程模型来描述,进程模型描述了进程在运行过程中在几个典型状态间的转化,这里介绍一下进程的三态模型。


进程在运行过程中不断改变状态,三态模型中进程具有三种基本状态。

1、就绪态

进程分配到除cpu外的所有资源,只要能获得cpu就能执行。

2、执行态

进程获得cpu正在执行。

3、阻塞态

进程因等待某事件而暂停执行。


三态模型可以用下图描述。


下面从一个具体的例子来说明一个进程的状态间转化,这里把事件(阻塞事件)设置为一具体事件(等待键盘输入)。例程的功能是在屏幕的第三行输出80个“a”,当输出到40个时,接受一个键盘输入,键盘输入完毕后,继续在第三行输出剩下的40个“a”。 代码如下:

#include <toyix.h>
main()
{
       int i;
       for(i = 0; i < 40; i++)
       {
              put_str(3,i,2,"a");
              delay(80);
       }
       get_char();
       for(i = 40; i < 80; i++)
       {
              put_str(3,i,2,"a");
              delay(80);
       }
}


编译、连接后,先用do命令加载这个程序。

“ready”后有“1”,表明进程已经处于就绪态。按任意键进程获得cpu执行(注:在其它系统中不用按键就开始执行了,为了演示方便toyix做了这种设计),running”显示了正在执行的进程的进程号,如下图:

这个进程在第三行不断的输出“a”。输出40个“a”后,进程等待键盘输入,阻塞了自己。“blocked”显示了阻塞进程的进程号,这个进程在屏幕上停止输出“a”,如下图:

为了让进程继续执行,只需按任意键发生键盘输入这个事件,唤醒这个进程。按下键盘后,如下图:

进程开始执行,这个进程在屏幕继续输出“a”,我们看到“running”显示在执行进程的进程号。这里需要注意的是,按键后进程被唤醒,并放入就绪队列,时间片轮到后马上被调度执行(Toyix是50毫秒左右轮转一次),因为时间比较短,所以我们看到进程马上开始执行了,而没有看到进程在就绪态的那个状态。


 

2.2 进程的创建


前面讲到“do”命令可以创建进程,创建进程后我们可以在进程运行时得到进程的进程号了, 程序如下:

#include <toyix.h>
main()
{
        int pid = get_pid();      /*得到进程的进程号PID*/
        printf("PID=%d",pid);
}

程序功能是得到当前进程的进程号,并打印出来, 运行结果如下图:

显示进程的进程号PID是1,因只有一个进程,所以分配的PID是1。
上面提到用do命令能创建一个或多个进程,那么在程序里怎么创建一个或多个进程呢? 操作系统必定提供了这样的调用接口,在toyix里创建进程主要有两个函数负责。

1、fork函数

2、frk函数

fork在英语里是“分叉”的意思,通过这个意思你也可以猜到它大概的功能,就是当执行的进程调用fork时,分成两个进程。这两个进程有密切的血缘关系(主要因为它们有许多相同的特性),创建新进程前的进程叫父进程,创建出的新进程叫子进程。下面例子使用fork后,分别输出每个进程的进程号PID。 代码如下:

#include <toyix.h>
main()
{
        int pid;
        fork();
        pid = get_pid();
        printf("PID=%d\n",pid);
}

运行结果如下图:

两个进程都输出了自己的进程号。那么谁是父进程,谁是子进程呢? 只有创建它的函数fork能区分,fork为了区分父进程和子进程对返回值做了处理,返回值在父进程中是进程号,在子进程中是0,那么就可以通过这个返回值来区分父子进程了,fork详细介绍请查阅函数手册。代码如下:

#include <toyix.h>
main()
{
       int i;
       i = fork();
       if(i > 0){ /*父进程*/
              printf("%d is %d father\n",get_pid(),i);
       }else if(i == 0) {/*子进程*/
              printf("%d is sun",get_pid());
       }else if(i == -1){    /*fork失败是返回-1*/
              printf("fork failed!");
       }
}

运行结果如下图:

可以看到,进程1是进程2的父进程,进程2是子进程。

Fork创建出子进程,父进程和子进程的PID是不同的,除了PID不同外,还有其它什么地方不同吗?我们可以从进程的组成来分析,进程一般由控制信息和本体信息组成。 PID属于进程的控制信息,进程的控制信息是不同的。 本体信息一般有代码段、数据段、栈段组成,栈段要保存cpu现场等进程特有信息,所以它不可能被父进程和子进程共享,从上面程序的局部变量i(在栈中分配)也可以看出栈段不共享,代码段和数据段是否相同可以用程序验证一下。先验证数据段,因为全局变量都在数据段里,所以对全局变量操作,就可以知道变量是否在同一数据段。代码如下:

#include <toyix.h>
int a = 0;
main()
{
        fork();
        a++;
        printf("PID=%d a=%d\n",get_pid(),a);
        delay(1000);   /*延迟1000毫秒*/
}

输出结果如下图:

a的值都是1,如果父进程和子进程共用同一数据段则a被加两次,应该为2,结果不是2,全局变量a在不同数据段。下面判断代码是否在同一代码段,只需把main的地址打印出来比较一下就可以了。代码如下:

#include <toyix.h>
main()
{
       fork();
       printf("PID=%d main= %x:%x\n",get_pid(),_CS,main);/*_CS是段寄存器CS*/
       delay(1000);
}

输出结果如下图:

父子进程代码部分的段地址不同,偏移地址相同,代码不在同一代码段。得出的结论: Fork创建的子进程和父进程的关系是:代码段、数据段、栈段都是独立的。

介绍了fork后看一下frk,frk与fork功能相似,但创建子进程与父进程共享数据段,这种设计是为了进程间交换数据的方便和高效,同时也减少了数据段复制的开销,为了区别于fork创建的进程,这种子进程叫它轻权进程。与fork一样,还是用全局变量简单验证一下数据段是否共享,代码如下:

#include <toyix.h>
int a = 0;
main()
{
        frk();
        a++;
        delay(100);
        printf("PID=%d a=%d\n",get_pid(),a);
        delay(100);
}

运行结果如图:

a的值是2,说明父进程和子进程都给同一全局变量a加了1,a是数据段的,结论是:frk创建的父子进程数据段是共享的。代码段是否相同,读者可以将fork验证程序改为frk试验一下就可以了,这里不再重复介绍,只给出结论:frk创建的轻权子进程和父进程代码段不是共享的。

Fork和frk虽然能创建进程,但是子进程功能还是和父进程相同,因为代码是相同的,为了能创建一个完全和父进程不同的进程,需要加载另一个程序。Toyix提供了exec函数,它的功能是创建一个进程并覆盖当前进程,这样可以通过创建的子进程调用exec生成新进程,就做到了进程A运行同时启动B进程了。

程序A:
#include <toyix.h>
main()
{
        int i = fork();
        if(i > 0)
        {
                printf("a father run!\n");
        }
        else
        {       exec("b.prg");
                printf("a sun run!");
        }
}

程序B:
#include <toyix.h>
main()
{
        printf("b run\n");
}

运行程序A:

"a sun run!"没有输出,说明A中的子进程被B覆盖了。

试试把exec放到主进程的情况:

#include <toyix.h>
main()
{
        int i = fork();
        if(i > 0)
        {
                int b = exec("b.prg");
                printf("a father run!%d\n",b);
        }
        else
        {      
                printf("a sun run!");
        }
}

运行结果如下图:

主进程被覆盖了。下面把fork换成frk,做相同的试验,注意它们之间的不同。

#include <toyix.h>
main()
{
        int i = frk();
        if(i > 0)
        {
                printf("a father run!\n");
        }
        else
        {       exec("b.prg");
                printf("a sun run!");
        }
}

输出结果如下图:

和fork不同的是“b run”没有输出,也就是b.prg没有运行,为什么没有运行呢? Frk有个特点就是主进程结束后,它创建的子进程会被撤销,这方面是和fork不同的,因为frk创建的子进程和父进程数据段共享,主进程撤销后,它的数据段也消失,如果它创建的轻权进程不被撤销,这个轻权进程使用数据段的数据时,会发生问题(数据段已被主进程释放),所以撤销轻权进程是合理的。为了轻权子进程不被撤销,可以在主进程中加入getch,让主进程接受键盘输入后再结束。代码如下:

#include <toyix.h>
main()
{
        int i = frk();
        if(i > 0)
        {
                printf("a father run!\n");
                getch();
        }
        else
        {       exec("b.prg");
                printf("a sun run!");
        }
}

运行结果如下图:

输出结果和fork的输出结果相同了。

上面是为了使frk创建的轻权进程不被撤销所以延迟了主进程的结束。如果轻权进程已经调用完了exec后,是怎样的情况呢? 我们可以先做一个推测,exec要创建一个进程来覆盖当前的进程(这里是轻权进程),而创建的进程要有自己的数据段,这个数据段不能和主进程共享,所以被创建的进程(它覆盖原来的轻权进程)的数据段是独立的,这样被覆盖的轻权进程就不再满足轻权进程的定义(没有了和主进程数据段共享), 被覆盖的轻权进程就升级为普通进程,这个普通进程不再依赖主进程的数据段,即使主进程被撤销,这个普通进程也不会被撤销。下面我们用例程验证我们的推断。

程序C

#include <toyix.h>
main()
{
        int i = frk();
        if(i > 0)
        {
                printf("c father run!\n");
                getch();
                printf("c father over!");
        }
        else
        {
                exec("d.prg");
                printf("c sun run!");
        }
}

程序D

#include <toyix.h>
main()
{
        int i;
        for(i = 0; i < 80; i++)
        {
                put_str(6,i,2,"a");
                delay(80);
        }

}

运行结果如下图:

我们在程序d.prg开始运行时,结束了主进程,d.prg进程没有结束,继续执行,这样验证了我们上面的推断是正确的。

再看看exec在主进程的状况。

#include <toyix.h>
main()
{
        int i = frk();
        if(i > 0)
        {
                exec("b.prg");
                printf("a father run!\n");
                getch();
        }
        else
        {      
                printf("a sun run!");
        }
}


运行结果 如下图:

和fork不同的是主进程没有被b.prg覆盖。Exec肯定是执行失败了,通过程序查看一下exec的返回值,函数手册里写明了exec失败时返回-1。

#include <toyix.h>
main()
{
        int i = frk();
        if(i > 0)
        {
                int j = exec("b.prg");
                printf("j = %d\n",j);
                printf("a father run!\n");
                getch();
        }
        else
        {      
                printf("a sun run!");
        }
}

输入结果如下图:

Exec的返回值是-1,说明在frk的主进程没能通过调用exec覆盖其自己的,是主进程特殊吗,还是主进程如果被覆盖会出问题?我们从程序的执行过程分析一下,主进程调用frk创建轻权子进程后,这个轻权子进程处于就绪态,主进程的代码继续执行,执行到exec,exec创建进程覆盖调用它的进程,如果此时exec执行成功,主进程被覆盖,它原来的数据段也就没有了,那么在就绪队列中的那个轻权子进程要使用数据段时,是不是要出错,所以exec执行失败。如果我们让主进程创建的轻权子进程不再使用主进程的数据段,看是否主进程能成功使用exec。轻权子进程不再使用主进程的数据段有两种办法,一种是它执行完毕被撤销,一种是通过调用exec升级为普通进程。我们用第一种测试一下。程序如下:

#include <toyix.h>
main()
{
        int i = frk();
        if(i > 0)
        {

                printf("a father run!");
                getch();/*等待子进程结束*/
                exec("b.prg");

        }
        else
        {
                printf("a sun run!\n");

                printf("a sun over!\n");
        }
}

我们看到,轻权子进程结束后主进程再调用exec成功加载了b.prg。

下面简单总结一下。

Fork和frk不同特性对比表

 

数据段共享

主进程结束后撤销所有子进程

主进程能被exec覆盖

fork

frk

是 (撤销的是轻权子进程)

否 (当有轻权子进程共享数据段时)

除了进程间并发执行,进程内的函数也可以并发执行,设置并发执行函数的系统调用是cobegin(是靠frk创建的轻权子进程实现的),使用时可以参考函数手册。 下面例程,函数f1在第三行输出“a”,f2在第五行输出“b”,cobegin启动两个轻权进程来分别执行f1和f2。

#include <toyix.h>
void f1()
{
        int i;
        for(i=0; i < 80; i++)
        {
                put_str(3,i,1,"a");
                delay(40);
        }
}
void f2()
{
        int i;
        for(i=0; i < 80; i++)
        {
                put_str(5,i,1,"b");
                delay(50);
        }
}
main()
{
        cobegin(f1,f2,0);
        getch();
}


需要注意的是cobegin和frk相似,创建进程共享数据段,主进程(调用cobegin的进程)结束后所有轻权子进程进程将被撤销,所以用getch控制主进程的结束。


 

2.3 进程间通信


进程并发运行就涉及到进程对资源的争夺问题,下面用向屏幕输出字符来说明这个问题。程序如下:

#include <toyix.h>
void f1()
{
        int i;
        for(i=1; i < 80; i++)
        {
                gotoxy(i,7);
                delay(60);
                printf("a");
               
        }
}
void f2()
{
        int i;
        for(i=1; i < 80; i++)
        {
                gotoxy(i,9);
                delay(250);
                printf("b");
        }
}

main()
{
        cobegin(f1,f2,0);
        getch();
}

这个程序实现的功能是,f1在7行输出“a”,f2在9行输出“b”。
输出结果如下图:

显然输出结果和我们想要的结果不一致。造成这种情况的原因是两个进程争夺光标并在光标处显示。如果某个进程得到光标资源,但此时被时间片用尽,等下次被执行时,光标位置被其它进程改了,继续使用就出现显示混乱。 所以要保证设置光标和输出的原子性,要保证原子性,就要用到P、V原语。把光标看作一个资源,则设置信号量为1,就代表有1个资源,使用资源时调用P,释放资源时调用V,这样让得不到资源的进程睡眠等待,就可以解决这个问题了。代码如下:

#include <toyix.h>
semaphore s;

void f1()
{
        int i;
        for(i=1; i < 80; i++)
        {
                p(&s);
                gotoxy(i,7);
                delay(60);
                printf("a");
                v(&s);
               
        }
}

void f2()
{
        int i;
        for(i=1; i < 80; i++)
        {
                p(&s);
                gotoxy(i,9);
                delay(250);
                printf("b");
                v(&s);
        }
}

main()
{
        set(&s,1);
        cobegin(f1,f2,0);
        getch();
}


运行结果如下图:

解决了争夺使用资源的问题,屏幕显示正常了。

对于屏幕这个资源,系统已经提供对于它互斥处理的函数了lock_screen和unlock_screen。
可以代替上面的PV原语解决方式。

#include <toyix.h>
void f1()
{
        int i;
        for(i=1; i < 80; i++)
        {
                lock_screen();
                gotoxy(i,7);
                delay(60);
                printf("a");
                unlock_screen();
               
        }
}

void f2()
{
        int i;
        for(i=1; i < 80; i++)
        {
                lock_screen();
                gotoxy(i,9);
                delay(250);
                printf("b");
                unlock_screen();
        }
}

main()
{
        cobegin(f1,f2,0);
        getch();
}

 

Toyix最主要的几个函数讲解完了,其它函数的使用可以参考函数手册