程序一:
int main(void) {
int pid1 = fork();
printf("**1**\n");
int psid2 = fork();
printf("**2**\n");
if (pid1 == 0) {
int pid3 = fork();
printf("**3**\n");
} else {
printf("**4**\n");
}
return 0;
}
程序执行过程:
Line 6: 创建子进程1,即主进程与子进程1共存。
Line 7: 主进程输出“**1**”
。
Line 8:主进程继续创建子进程2,即主进程与两个子进程共存。
Line 9:主进程输出“**2**”
。
Line 14:主进程输出“**4**”
。
Line 7:子进程输出“**1**”
。
Line 8:子进程1创建子进程3,即三个子进程共存。
Line 9:子进程1输出“**2**”
。
Line 11:子进程1继续创建子进程4,即四个子进程共存。
Line 12:子进程1输出“**3**”
。
Line 9 :子进程2输出“**2**”
。
Line 14: 由于子进程2的父进程中pid1不为0,所以输出“**4**”
。
Line 9 : 子进程3输出“**2**”
。
Line 11:由于子进程3的父进程中pid1为0,所以创建子进程5。
Line 12:子进程3输出“**3**”
。
Line 12:子进程4输出“**3**”
。
Line 12: 子进程5输出“**3**”
。
输出结果:
**1**
**1**
**2**
**4**
**2**
**3**
**3**
**2**
**3**
**2**
**4**
**3**
输出结果的顺序与我的分析不同,原因是进程的执行是抢占式的,哪个进程抢占到了CPU,哪个进程就执行输出。但是输出的内容是一样的:2个“**1**”
,4个“**2**”
,4个“**3**”
,2个“**4**”
.
程序二:
int main(void) {
pid_t pid;
if ((pid = fork()) == -1) { // 生成子进程1
printf("Error");
exit(-1);
}
if (pid != 0) {
pid_t pid1;
if ((pid1 = fork()) == -1) { // 生成子进程2
printf("Error");
exit(-1);
}
if (pid1 != 0) {
printf("a");
} else {
printf("b");
exit(0);
}
} else {
printf("c");
exit(0);
}
wait(0); // 等待子进程执行完毕
wait(0);
exit(0); // 主进程退出
}
该程序是主进程与两个子进程并发执行的过程。其中主程序输出a,子程序分别输出b、c。
输出结果:
cba
也可用类似于程序一中的进程树进行分析。
程序三:
int main(void) {
int a = 0;
pid_t pid;
if ((pid=fork())) {
a = 1;
}
for (int i = 0; i < 2; i++) {
printf("X");
}
if (pid == 0) {
printf("%d\n", a);
}
return 0;
}
程序的执行过程:
Line 8: 调用fork()生成子进程1,即父进程与子进程1共存;
Line 9: 父进程执行a = 1;
Line 11: 父进程执行两次循环,输出两个“X”;
Line 11: 子进程执行两次循环,输出两个“X”;
Line 15: 子进程输出a的值,即“0”
输出结果:
XXXX0
即输出四个X,一个0
也可用类似于程序一中的进程树进行分析。
程序四:
int main(void) {
int a = 0;
pid_t pid[2];
for (int i = 0; i < 2; i++) {
if ((pid[i]=fork())) {
a = 1;
}
printf("X");
//fflush(stdout);
}
if (pid[0] == 0) printf("%d\n", a);
if (pid[1] == 0) printf("%d\n", a);
return 0;
}
程序的执行过程:
Line 9: 通过第一次循环调用fork()函数创建子进程1,并将a赋值为1,输出“X”;i=0
Line 9: 通过第二次循环调用fork()函数创建子进程2,并将a赋值为1,输出“X”;i=1
Line 12: 子进程1输出“X”;
Line 9: 子进程1创建子进程3,并将a赋值为1;
Line 12: 子进程1输出“X”;
Line 14: 子进程1输出a的值,即“1”;
Line 12: 子进程2输出“X”;
Line 14:由于子进程2的父进程的的pid[0]!=0,不输出,继承父进程的a=1(第一个循环中改变)
Line 15:输出a 的值,即“1”。
Line 12:子进程3输出“X”;
Line 14:由于子进程3的父进程的pid[0]==0,输出a的值,即“0”;
Line 15:输出a的值,即为“0”。
输出结果:
XXXX1
XX1
XX0
0
按照我们的分析,程序应当输出6个X,2个1,2个0。可如今程序输出了8个X,这是为什么呢?
现在我们来假设多出的两个x来自于哪里:
对于printf函数来说,如果输出没有换行,则输出的内容会残留在缓冲区中,直到下一个回撤出现时清空。
在建立子进程三的时候,也就是主进程执行第二次循环的时候,由于主进程第一次循环输出“X”而没有换行,所以其缓冲区中存在“X”,所以子进程的缓冲区中也存在了“X”,待会输出时也会输出这个“X”。这是第一个“X”。
现在我们来验证一下我们的假设:
在printf("X");
后面加上一句fflush(stdout);
,这一句起到清空缓存区的作用,下面我们再编译执行函数,结果如下:
输出结果:
XXX1
XX1
X0
0
果然,清除了缓冲区,程序就如我们预想结果一致了~
也可用类似于程序一中的进程树进行分析。
程序一:
void waiting();
void stop();
int wait_mark;
int main(void) {
int p1, p2;
while((p1=fork())==-1); // 创建子进程1
if (p1 > 0) {
while((p2=fork())==-1); // 创建子进程2
if (p2 > 0) {
printf("%d %d %d\n", getpid(), p1, p2);
wait_mark = 1;
//signal(SIGINT, SIG_IGN);
signal(SIGINT, stop); // 处理ctrl+c的信号
waiting();
kill(p1, 16); // 向进程1发送16的信号
kill(p2, 17); // 向进程2发送17的信号
wait(0); // 等待子进程执行完毕
wait(0); // 等待子进程执行完毕
printf("parent process is killed!\n");
exit(0);
} else {
wait_mark = 1;
signal(SIGINT,SIG_IGN); // 忽略ctrl+c的影响,标注A
signal(17, stop); // 处理来自主进程的17信号
waiting();
printf("child process 2 is killed by parent!\n");
exit(0);
}
} else {
wait_mark = 1;
signal(SIGINT, SIG_IGN); // 忽略ctrl+c的影响,标注B
signal(16, stop); // 处理来自主程序的16信号
waiting();
printf("child process 1 is killed by parent!\n");
exit(0);
}
}
void waiting() {
while(wait_mark!=0);
}
void stop() {
wait_mark = 0;
}
对于程序的分析,我已用注释表明,下面是对实验结果的分析。
最开始程序是没有标注A、标注B这两个语句的,其运行的结果与我们预想的不同。
我们预想的结果是:(按下ctrl+C)
^Cchild process 1 is killed by parent!
child process 2 is killed by parent!
parent process is killed!
实际的结果如下:(按下ctrl+C)
^Cparent process is killed!
为什么实际的结果与我们预想的不一样呢?因为当我们程序运行后,无论是主进程还是子进程,都卡在了waiting()函数这里等待中断信号。一旦我们按下ctrl+c,系统会向主进程和两个子进程同时发送SIGINT信号。对于主进程,根据设置,执行stop函数;对于子进程,由于没有设置对该信号的处理,所以默认执行exit()函数而不输出。无论主进程怎么发出信号,子进程也是无法响应的。
解决以上问题的关键方法就是:让子进程屏蔽SIGINT信号。其代码如上所示。
或者不加标注A、标注B两个语句,让程序输出主进程的pid(加入我得到的是10156).我们打开一个新的终端,输入kill -SIGINT 10156
来中断主程序,然后查看原终端,发现能得到一样的结果。
程序二:
void waiting();
void stop();
int wait_mark;
int main(void) {
int p1, p2;
signal(SIGINT,SIG_IGN);
// ctrl + c
signal(SIGQUIT, SIG_IGN);
// ctrl + \
while((p1=fork())==-1);
if (p1 > 0) {
while((p2=fork())==-1);
if (p2 > 0) {
wait_mark = 1;
signal(SIGINT, stop);
waiting();
kill(p1, 16);
kill(p2, 17);
wait(0);
wait(0);
printf("parent process is killed!\n");
exit(0);
} else {
wait_mark = 1;
signal(17, stop);
waiting();
printf("child process 2 is killed by parent!\n");
exit(0);
}
} else {
wait_mark = 1;
signal(16, stop);
waiting();
printf("child process 1 is killed by parent!\n");
exit(0);
}
return 0;
}
void waiting() {
while(wait_mark!=0);
}
void stop() {
wait_mark = 0;
}
要使程序彻底忽略ctrl+C信号,我们可以在main函数一开始就设置signal函数,或者是将主进程中的信号设置替换为signal(SIGINT,SIG_IGN);
而不执行stop函数。当然还可以加入signal(SIGQUIT, SIG_IGN);
来屏蔽ctrl+\
信号。
# include
# include
# include
# include
# include
# include
# include
# define MAX_SEQUENCE 10
typedef struct {
long fib_sequence[MAX_SEQUENCE];
int sequence_size;
} shared_data;
int main(int argc, char* argv[]) {
if (argc != 2) { // 判断是否输入了长度
fprintf(stderr, "Please enter the length of sequence\n");
exit(-1);
}
//int seq_size = argv[1]-'0';
int seq_size = atoi(argv[1]); // 将字符串转化为整型
if (seq_size > MAX_SEQUENCE) { // 判断输入的长度是否合法
fprintf(stderr, "Please enter the length less than 11\n");
}
int segment_id; // 创建或打开共享内存
if ((segment_id =shmget(IPC_PRIVATE, sizeof(shared_data), S_IRUSR| S_IWUSR)) == -1) {
fprintf(stderr, "Unable to create share memoriy");
exit(-1);
}
shared_data* shared_memory; // 获取共享内存地址
if ((shared_memory = (shared_data*)shmat(segment_id, 0, 0)) == (shared_data*)-1) {
fprintf(stderr, "Unable to attach to segment%d\n", segment_id);
exit(-1);
}
shared_memory->sequence_size = seq_size;
pid_t pid; // 创建子进程
if ((pid = fork()) == -1) {
fprintf(stderr,"Unable to create a new process\n");
exit(-1);
}
if (pid == 0) { // 子进程生成斐波那契数列
shared_memory->fib_sequence[0] = 0;
shared_memory->fib_sequence[1] = 1;
for (int i = 2; i < seq_size; i++) {
shared_memory->fib_sequence[i] = shared_memory->fib_sequence[i-1]+shared_memory->fib_sequence[i-2];
}
if (shmdt(shared_memory) == -1) { // 断开共享内存连接
fprintf(stderr, "Unable to detach");
exit(-1);
}
} else { // 主进程输出斐波那契数列
wait(0);
for (int i = 0; i < seq_size; i++) {
printf("%ld ", shared_memory->fib_sequence[i]);
}
printf("\n");
if (shmdt(shared_memory) == -1) { // 断开共享内存连接
fprintf(stderr, "Unable to detach");
exit(-1);
}
shmctl(segment_id, IPC_RMID, NULL); // 删除共享内存
exit(0);
}
}
对程序的分析在代码注释中已经写的很明白了。现在来谈谈实现的过程。
# define MAX_LINE 80
# define BUFFER_SIZE 50
int next = 0; // 下一个指令存放的下标
char* history[10][MAX_LINE/2+1]; // 存放历史记录
int CommandLength[10] = {0}; // 标识指令的长度
void ProcessRCommand(char *args[]) { // 处理R指令的函数
int i, j, count=10;
char* newargs[MAX_LINE/2+1];
for(i = 0; i < MAX_LINE/2+1; ++i) {
newargs[i] = (char*)malloc((MAX_LINE/2+1)*sizeof(char));
}
history[next][0] = '\0';
if (args[1] == NULL){
i = (next + 9) % 10;
for(j = 0; j < CommandLength[i]; ++j){
strcpy(newargs[j], history[i][j]);
}
newargs[j]=NULL;
execvp(newargs[0], newargs);
} else {
i = next;
while (count--){
i = (i + 9) % 10;
if (strncmp(args[1], history[i][0], 1) == 0){
for(j = 0; j < CommandLength[i]; ++j) {
strcpy(newargs[j], history[i][j]);
}
newargs[j]=NULL;
execvp(newargs[0], newargs);
}
}
}
}
void setup(char inputBuffer[], char* args[], int* background) { // 指令的读取
int length; // length:命令的字符数目
int i; // i:循环变量
int start; // start:命令的第一个字符位置
int ct; // ct:下一个参数存入args[]的位置
ct = 0;
length = read(STDIN_FILENO, inputBuffer, MAX_LINE);
start = -1;
if (length == 0) exit(0);
if (length < 0) {
perror("error reading the command");
exit(-1);
}
for (i = 0; i < length; i++) {
switch(inputBuffer[i]) {
case ' ':
case '\t':
if (start != -1) {
args[ct] = &inputBuffer[start];
ct++;
}
inputBuffer[i] = '\0'; // 起到分割作用
start = -1;
break;
case '\n':
if (start != -1) {
args[ct] = &inputBuffer[start];
ct++;
}
inputBuffer[i] = '\0';
args[ct] = NULL;
break;
default:
if (start == -1) {
start = i;
}
if (inputBuffer[i] == '&') {
*background = 1;
inputBuffer[i] = '\0';
}
}
}
args[ct] = NULL; // 不需要知道有多少个参数便能实现复制
}
void handle_SIGINT() { // 对CTRL+C的信号处理
int i, j;
printf("\n");
for (i = 0; i < 10; i++) {
for (j = 0; j < CommandLength[i]; j++) {
printf("%s ", history[i][j]);
}
printf("\n");
}
printf("COMMAND->");
fflush(stdout);
}
int main(void) {
char inputBuffer[MAX_LINE]; // 用于存储指令
int background; // 用于标识子进程是否能与父进程并行
char* args[MAX_LINE/2+1]; // 用于存储被切割后的指令
pid_t pid;
int i, j;
for(i = 0; i < 10; ++i) { // 为存储历史记录的函数分配空间
for(j = 0; j < MAX_LINE/2+1; ++j) {
history[i][j] = (char*)malloc(40*sizeof(char));
}
}
signal(SIGINT, handle_SIGINT); // 捕捉信号
while(1) { // 实现shell的输入执行循环
background = 0;
printf("COMMAND->");
fflush(stdout);
setup(inputBuffer, args, &background);
i = 0;
if (args[0] != NULL && strcmp(args[0],"r") != 0){ // 记录非r型指令
while(args[i] != NULL) {
strcpy(history[next][i], args[i]);
++i;
}
CommandLength[next] = i;
next = (next + 1) % 10;
}
if (args[0] != NULL && strcmp(args[0],"r") == 0) { // 记录r型指令
if (args[1] == NULL) { // 记录无参数的r型指令
i = (next + 9) % 10; // 获取最后一条历史指令的下标
for(j = 0; j < CommandLength[i]; ++j) {
strcpy(history[next][j], history[i][j]);
}
CommandLength[next] = j;
next = (next + 1) % 10;
} else { // 记录有参数的r型指令
i = next;
int count = 10;
while(count--) {
i = (i + 9) % 10;
// 匹配指令第一个字母与第一个参数相同的指令
if (strncmp(args[1], history[i][0], 1) == 0) {
for(j = 0; j < CommandLength[i]; ++j) {
strcpy(history[next][j], history[i][j]);
}
CommandLength[next] = j;
next = (next + 1) % 10;
break;
}
}
}
}
if ((pid=fork()) == -1) { // 生成子进程
printf("Fork Error.\n");
}
if (pid == 0) { // 子进程执行的内容
if(strcmp(args[0],"r") == 0){ // 识别r型指令并调取处理函数
ProcessRCommand(args); // 处理r型指令
exit(0);
} else{ // 执行非r型指令
execvp(args[0],args);
exit(0);
}
}
if (background == 0) {
wait(0);
}
}
}
运行shell:
(1)输入指令:
(2)ctrl+c 以及r指令的执行:
(1)通过本次实验,我加深了对进程概念的理解:进程本质上就是程序的一次执行过程;
(2)并且进一步认识了并发执行的实质:减少程序的顺序性,提高系统的并行性;
(3)了解到signal()能捕捉信号并作出相应的处理;
(4)了解到进程间通信的其中一种方式:进程间共享内存;
(5)通过实现shell,了解到shell执行指令时使用的系统调用,对shell有了进一步的认识;
(6)对于实验一,我了解到若printf输出的内容没有换行,那么输出的内容就仍然保留在缓冲区中,因而在fork时会复制到子进程中;
(7)对于实验二,我了解到主进程与子进程处于“waiting”状态时,即处于等待信号的状态时,一旦我发出SIGINT信号,主进程与子进程都能够接收到。为了使主进程接收到信号并处理而子进程不处理,那么需要给子进程中加入信号屏蔽语句;
(8)对于实验三,我了解到了主进程与子进程间实现数据共享的方式—共享内存;
(9)对于实验四,我了解到了一个简单版的shell是如何实现的,如何实现它的指令执行,以及如何实现它历史记录的查询、执行。
总而言之,本次操作系统实验让我受益匪浅。