由于时间原因,我没有写编程题,之后有时间的话我会补在后面~

论述长期调度、中期调度和短期调度的差异

  • 短期调度:在内存作业中选择就绪执行的作业,并为他们分配CPU
  • 中期调度:作为一种中等程度的调度程序,尤其被用于分时系统,将部分运行程序移出内存,之后,从中断处继续执行
  • 长期调度:确定哪些作业调入内存以执行

它们之间的主要差异在于执行频率,短期调度必须经常调用一个新的进程,由于在系统中,长期调度处理移动作业时,并不频繁被调用,可能在进程离开系统时才被唤起。

内核采取一些行动以便在两个进程间进行上下文切换,描述一下

首先将当前进程的信息保存在PCB表中,然后发出中断,开始进行另一个进程,等这个进程结束之后,通过PCB表,恢复另一个进程的状态。

构建一个类似图3-8的进程树,采用命令ps -ael,可以获取UNIX或Linux系统的进程信息。采用命令man ps,可以获取关于命令ps的更多信息,Windows系统的任务管理器没有提供父进程ID,但是提供进程监控工具。

这个题大家在纸上随便画画、试试命令就好了。

针对UNIX和Linux系统的进程init在进程终止方面的作用,请解释一下

init进程是UNIX和Linux系统内进程树的根进程,init进程会定期调用wait(),以便手机任何孤儿进程(父进程没有调用wait就终止的进程称为孤儿进程)的退出状态,并释放孤儿进程标识符和进程表条目。

下面的程序创建了多少个进程(包括初始的父进程)

#include<stdio.h>
​
int main() {    
    int i;
    for(i = 0; i < 4; ++i) {
        fork();
    }
    return 0;
}

直接输出getpid,然后统计看有多少个不重复的就好了

#include<stdio.h>

int main() {    
    printf("%d\n", getpid());
    int i;
    for(i = 0; i < 4; ++i) {
        fork();
        printf("%d\n", getpid());
    }
    return 0;
}

一共有16个独立的进程

这里补充一下原因:每次fork一个子进程后,它都分享父进程的内存区域,所以每个fork出的子进程会接着执行循环。

执行第一次的时候,fork出一个进程,第二次执行的时候,连同上次的子进程也会产生子进程,故而1变2,2变4,4变8......,最终是\(2^n - 1\)个进程,加上本来就有的父进程所以一共是\(2^n\)个进程

对下面的程序中,标记为printf("LINE J")的行所能执行的环境,请解释一下

#include<stdio.h>

#include<sys/types.h>
#include<stdio.h>
#inlcude<unistd.h>

int main() {
    pid_t pid;
    
    if(pid < 0) {
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if(pid == 0) {
        execlp("/bin/ls", "ls", NULL);
        printf("LINE J");
    }
    else {
        wait(NULL);
        printf("Child complete");
    }
    return 0;
}
#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>

int main() {
    pid_t pid;
    pid = fork();
    if(pid < 0) {
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if(pid == 0) {
        printf("LINE I\n");    //多加一行,最终输出,说明这一步确实执行了
        execlp("/bin/ls", "ls", NULL);
        printf("LINE J\n");   //这一行并没有执行,说明程序提前终止了
    }
    else {
        wait(NULL);
        printf("Child complete");
    }
    return 0;
}

会输出LINE I,然后输出文件列表,最后输出Child complete,不会输出LINE J,原因如下:

子进程创建之后,会调用execlp函数,进程自己的执行代码就会变成加载程序的代码,execlp()后边的代码也就不会执行了,所以printf("LINE J");不会被执行,除非exclp函数出错,printf才会被执行。

采用如下程序,确定行A、B、C、D中的pid值(假定父进程和子进程的pid分别为2600和2603)

#include<stdio.h>
#include<stdio.h>
#include<unistd.h>

int main() {
    pid_t pid, pid1;
    pid = fork();
    
    if(pid < 0) {
        fprintf(stderr, "Fork Failed");
        return 1;
    }
    else if (pid == 0) {
        pid1 = get_pid();   //子进程的pid
        printf("child:pid = %d", pid);   /* A */
        printf("child:pid1 = %d", pid1); /* B */
    }
    else {
        pid1 = getpid();
        printf("parent:pid = %d", pid); /* C */
        printf("parent:pid1 = %d", pid1); /* D */
        wait(NULL);
    }
    return 0;
}

首先分析A,进入子进程后,pid为0(书上第81面上半部分),所以A为0,然后pid1为当前子进程的pid值,也就是题目给的2603。

然后子进程结束,返回父进程,在前面pid=fork(),这句话是创建子进程,并返回子进程的pid值,所以C行会输出子进程的pid,也就是2603,pid1为当前进程的pid值,也就是题目中给的父进程的pid值,为2600。

所以会输出:

child:pid = 0child:pid1 = 2603parent:pid = 2603parent:pid1 = 2600

普通管道有时比命名管道更适合,而命名管道有时比普通管道更合适,请举例说明

  • 普通管道:只允许单向通信,通信双方必须为父子进程,一端为写入端、另一端为读出端,如果需要双向通信,则需要采用两个管道。一旦进程完成通信并且终止,那么普通管道就不存在了。
  • 命名管道:允许双向通信,并且父子关系不必须,建立了一个命名管道后,多个进程都可用它通信。当通信完成后,命名管道继续存在。
  • 当只涉及到简单的父子进程通信,且仅需要单向通信时,普通管道会更加方便
  • 如果涉及到多个进程之间的通信,且需要通信管道可持久化,那么命名管道会更加方便。

对于RPC机制,若没有强制“最多一次”或“正好一次”的语义,描述一下所带来的一些不必要的后果,讨论一下没有这些强制保证的可能用途

RPC可能执行失败或者多次重复执行,考虑一个远程程序在一个不支持这种语义的系统上从一个银行上取钱,远程程序的一次调用将会导致从服务器上多次执行取钱操作或者操作失败。

使用下面的程序,请解释一下行X和Y的输出是什么

#include<sys/types.h>
#include<stdio.h>
#include<unistd.h>

#define SIZE 5
int nums[SIZE] = {0, 1, 2, 3, 4};

int main() {
    int i;
    pid_t pid;
    pid = fork();
    
    if(pid == 0) {
        for(i = 0; i < SIZE; ++i) {
            nums[i] *= -i;
            printf("CHILD:%d ", nums[i]); /* LINE X */
        }
    }
    else if(pid > 0) {
        wait(NULL);
        for(i = 0; i < SIZE; ++i) {
            printf("PARENT:%d ", nums[i]); /* LINE Y */
        }
    }
    return 0;
}

输出结果:CHILD:0 CHILD:-1 CHILD:-4 CHILD:-9 CHILD:-16 PARENT:0 PARENT:1 PARENT:2 PARENT:3 PARENT:4

也就是说,系统调用子进程对数组的影响,并没有作用到父进程上,这是因为由于子进程是父进程的副本,任何发生在子进程中的改变将出现在它自己的数据拷贝上,并不会影响父进程。

下面设计的优缺点是什么?系统层次和用户层次都要考虑

  1. 同步和异步通信
    1. 同步通信的影响是它允许发送者和接收者之间有一个集合点。缺点是阻塞发送时,不需要集合点,而消息不能异步传递。因此,消息传递系统往往提供两种形式的同步。
  2. 自动和显示缓冲
    1. 自动缓冲提供了一个无限长度的队列,保证了发送者在复制消息时不会遇到阻塞,因为没有自动缓冲的相关规范,所以一种方案是开足够大的空间,但是这些空间可能会被浪费。
    2. 显示缓冲指出了缓冲区的大小,这种情况下,发送方在等待队列中可用的空间时可能会被阻塞,这种方式不太可能会浪费空间。
  3. 复制传送和引用传送
    1. 复制传递和引用传递:复制传递不郧西接收者改变参数的状态,引用传递可以。引用传递的优点之一在于它允许程序员写一个分布式版本的集中应用程序。
  4. 固定大小和可变大小
    1. 固定大小和可变大小的消息的实现主要与缓冲的问题相关。对于固定大小的消息,具有特定大小的缓冲区可以容纳已知的数量的消息,而可变大小并不清楚。
    2. 参考windows 2000的处理方式:对于固定大小的消息(小于256字节),从发送方的地址空间复制到接受进程的地址空间。对于更大的消息,采用共享内存传递,这种方式对效率的提升很明显,也可以减少地址空间的浪费。