OSTEP-第五章

第五章课后题

第1题

编写一个调用 fork()的程序。在调用 fork()之前,让主进程访问一个变量(例如 x)
并将其值设置为某个值(例如 100)。子进程中的变量有什么值?当子进程和父进程都改变
x的值时,变量会发生什么?

x = 100是fork前的,fork后的x彼此独立

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

int main(){
  int x ;
  x = 100;
  int rc = fork();
  if(rc == 0){
    printf("子进程的x:%d\n",x);
    x = 30;
    printf("子进程的新x:%d\n",x);
  }else if(rc > 0){
    printf("父进程的x:%d\n",x);
    x = 13;
    printf("父进程的新x:%d\n",x);
  }
}

输出:

父进程的x:100
父进程的新x:13
子进程的x:100
子进程的新x:30

结果符合预期

第2题

编写一个打开文件的程序(使用 open()系统调用),然后调用 fork()创建一个新进程。
子进程和父进程都可以访问 open()返回的文件描述符吗?当它们并发(即同时)写入文件时,
会发生什么?

用系统调用写程序真是……很“享受”了

先复习一下open函数:

int open(const char *pathname, int flags, mode_t mode);

比如:

int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);

含义就是,只写、不存在则创建、存在则清空的方式打开test.txt,创建新文件则以0644的权限,即创建者可读写其余只读

#include<stdio.h>
#include<stdlib.h>
#include <unistd.h>
#include<fcntl.h>
#include<string.h>

int main(){
  int fd = open("test.txt", O_WRONLY | O_CREAT | O_TRUNC, 0644);
  int rc = fork();
  const char *msg1 = "hello world\n";
  const char *msg2 = "aloha world\n";
  if(rc == 0){
    printf("子进程写入%d字节\n",write(fd, msg1, strlen(msg1)));
  }else if(rc > 0){
    printf("父进程写入%d字节\n",write(fd, msg2, strlen(msg1)));
  }
}

运行结果:

父进程写入12字节
子进程写入12字节
aloha world
hello world

结果表明文件标识符都可以使用,且同步的写入都执行了,没遇到冲突

第3题

用 fork()编写另一个程序。子进程应打印“hello”,父进程应打印“goodbye”。你
应该尝试确保子进程始终先打印。你能否不在父进程调用 wait()而做到这一点呢?

正常来说wait就是用来控制顺序的,使用wait就是最好的办法。如果不使用wait的话,就需要两个进程有一些交流。

进程间通信,也就是IPC机制,除了wait还有信号,管道,或者共享内存等等

对此题来说,也许还可以简单粗暴地sleep一个足够长的时间,比如1s?

但是上一题暗示我们还可以利用磁盘(文件)实现交流,虽然冷门了点,但试试还是很有趣的,不是吗:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <sys/stat.h>

int main() {
  unlink("test.txt");

  int fd = open("test.txt", O_RDWR | O_CREAT | O_TRUNC, 0644);

  int rc = fork();

  if (rc == 0) {
    // 子进程
    printf("hello\n");
    fflush(stdout);
    const char *msg = "OK";
    write(fd, msg, strlen(msg));
    fsync(fd); 
  } else if (rc > 0) {
    // 父进程
    char buffer[10];
    ssize_t bytes_read;

    while (read(fd, buffer, sizeof(buffer) - 1)<= 0) {
      // 重置文件指针到开头
      lseek(fd, 0, SEEK_SET);
    }

    printf("goodbye\n");
    fflush(stdout);
  }

  close(fd);
  return 0;
}

因为交流简单,写得糙了点。文件要以读写权限打开,以及注意读完要回到开头

第4题

编写一个调用 fork()的程序,然后调用某种形式的 exec()来运行程序/bin/ls。看看是 否可以尝试 exec()的所有变体,包括 execl()、execle()、execlp()、execv()、execvp()和 execvP()。
为什么同样的基本调用会有这谁多变种?

这题……交给大模型了:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/wait.h>

void test_execl() {
    printf("=== 测试 execl() ===\n");
    printf("execl() 接受一个参数列表,以 NULL 结尾\n");

    int rc = fork();
    if (rc == 0) {
        // 子进程:使用 execl
        execl("/bin/ls", "ls", "-l", "/", NULL);
        // 如果执行到这里说明 exec 失败了
        perror("execl failed");
        exit(1);
    } else if (rc > 0) {
        // 父进程:等待子进程结束
        wait(NULL);
        printf("execl() 测试完成\n\n");
    } else {
        perror("fork failed");
    }
}

void test_execle() {
    printf("=== 测试 execle() ===\n");
    printf("execle() 接受一个参数列表和环境变量数组\n");

    char *envp[] = {"PATH=/bin:/usr/bin", "USER=testuser", NULL};

    int rc = fork();
    if (rc == 0) {
        // 子进程:使用 execle
        execle("/bin/ls", "ls", "-l", "/tmp", NULL, envp);
        perror("execle failed");
        exit(1);
    } else if (rc > 0) {
        wait(NULL);
        printf("execle() 测试完成\n\n");
    } else {
        perror("fork failed");
    }
}

void test_execlp() {
    printf("=== 测试 execlp() ===\n");
    printf("execlp() 接受一个参数列表,在 PATH 中查找程序\n");

    int rc = fork();
    if (rc == 0) {
        // 子进程:使用 execlp(不需要完整路径)
        execlp("ls", "ls", "-l", "/usr", NULL);
        perror("execlp failed");
        exit(1);
    } else if (rc > 0) {
        wait(NULL);
        printf("execlp() 测试完成\n\n");
    } else {
        perror("fork failed");
    }
}

void test_execv() {
    printf("=== 测试 execv() ===\n");
    printf("execv() 接受一个参数数组\n");

    char *args[] = {"ls", "-l", "/home", NULL};

    int rc = fork();
    if (rc == 0) {
        // 子进程:使用 execv
        execv("/bin/ls", args);
        perror("execv failed");
        exit(1);
    } else if (rc > 0) {
        wait(NULL);
        printf("execv() 测试完成\n\n");
    } else {
        perror("fork failed");
    }
}

void test_execvp() {
    printf("=== 测试 execvp() ===\n");
    printf("execvp() 接受一个参数数组,在 PATH 中查找程序\n");

    char *args[] = {"ls", "-l", "/var", NULL};

    int rc = fork();
    if (rc == 0) {
        // 子进程:使用 execvp(不需要完整路径)
        execvp("ls", args);
        perror("execvp failed");
        exit(1);
    } else if (rc > 0) {
        wait(NULL);
        printf("execvp() 测试完成\n\n");
    } else {
        perror("fork failed");
    }
}

void test_execvpe() {
    printf("=== 测试 execvpe() ===\n");
    printf("execvpe() 接受一个参数数组、在 PATH 中查找程序,并指定环境变量\n");
    printf("注意:这个函数可能在某些系统上不可用\n");

    char *args[] = {"ls", "-l", "/opt", NULL};
    char *envp[] = {"PATH=/bin:/usr/bin", "USER=testuser", NULL};

    int rc = fork();
    if (rc == 0) {
        // 子进程:使用 execvpe
        // 注意:execvpe 在某些系统上可能不存在
        #ifdef _GNU_SOURCE
        execvpe("ls", args, envp);
        #else
        printf("execvpe() 在此系统上不可用,使用 execve() 替代\n");
        execve("/bin/ls", args, envp);
        #endif
        perror("execvpe/execve failed");
        exit(1);
    } else if (rc > 0) {
        wait(NULL);
        printf("execvpe() 测试完成\n\n");
    } else {
        perror("fork failed");
    }
}

int main() {
    printf("演示各种 exec() 变体\n");
    printf("===================================\n\n");

    test_execl();
    test_execle();
    test_execlp();
    test_execv();
    test_execvp();
    test_execvpe();

    printf("===================================\n");
    printf("为什么有这么多 exec() 变体?\n\n");
    printf("1. 参数传递方式的区别:\n");
    printf("   - 'l' 结尾:参数作为列表传递(list)\n");
    printf("   - 'v' 结尾:参数作为数组传递(vector)\n\n");
    printf("2. 程序查找方式的区别:\n");
    printf("   - 'p' 结尾:在 PATH 环境变量中查找程序\n");
    printf("   - 无 'p':需要提供程序的完整路径\n\n");
    printf("3. 环境变量的处理:\n");
    printf("   - 'e' 结尾:可以指定自定义环境变量\n");
    printf("   - 无 'e':继承当前进程的环境变量\n\n");
    printf("这种设计提供了灵活性,让程序员可以根据具体需求选择\n");
    printf("最合适的变体,避免不必要的复杂性。\n");

    return 0;
}

解释得很清楚了

第5题

现在编写一个程序,在父进程中使用 wait(),等待子进程完成。wait()返回什么?如
果你谁子进程中使用 wait()会发生什么?

写之前猜测一下:wait的返回值不会是等待的时间吧? 如果父等子,子等父,那就死锁了

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

int main(){
  int rc = fork();
  if(rc == 0){
    float temp = 0.0;
    for(int i = 0 ; i < 1e3 ; i++){
       temp += (0.3 * i);
    }
  }else if(rc > 0){
    printf("%d\n",wait(NULL));
    printf("%d",rc);
  }
}

经检验,wait的返回值是子进程的pid

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

int main(){
  int rc = fork();
  if(rc == 0){
    wait(NULL);
  }else if(rc > 0){
    printf("%d\n",wait(NULL));
    printf("%d",rc);
  }
}

第二个猜测也错了,并没有死锁。其实也是合理的,wait函数的参数是 int *wstatus,一个状态,也就是说
wait的对象默认是子进程,而子进程没fork是没有子进程的

第6题

对前一个程序稍作修改,这次使用 waitpid()而不是 wait()。什么时候waitpid()会有用?

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

int main(){
    int rc = fork();
    if(rc == 0){
        for(int i = 1; i <= 20; i++){
            printf("计时%d ",i);
        }
    }else if(rc > 0){
        printf("%d\n", waitpid(rc, NULL, 0));
    }
}

waitpid显式指明了等的进程,如果一个父进程fork了很多次,那wait就会有歧义

但是个人感觉最重要的还是含义清晰

第7题

编写一个创建子进程的程序,然后在子进程中关闭标准输出(STDOUT_FILENO)。
如果子进程谁关闭描述符后调用 printf()打印输出,会发生什么?

#include<stdio.h>
#include<sys/wait.h>
#include <unistd.h>
#include <fcntl.h>

int main(){
  int rc = fork();
  if(rc == 0){
    close(1);
    printf("hello world from child\n");
  }else if(rc > 0){
    wait(NULL);
    printf("hello world from parent \n");
  }
}

输出:

hello world from parent

只有一行

第8题

编写一个程序,创建两个子进程,并使用 pipe()系统调用,将一个子进程的标准输
出连接到另一个子进程的标准输入。

pipe,管道,一种IPC机制

  • ‘pipe()’,在unistd.h库中。创建一个管道,记录两个文件标识符,一端读,一端写
  • ‘dup2()’,重定向标识符,接受两个文件标识符,将第二个变成第一个的副本

借助一个创建的管道,用两次dup2,把两个子进程的输入输出连到一块

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int pipefd[2];
    pipe(pipefd); // pipefd[0] - read, pipefd[1] - write

    pid_t pid1 = fork();

    if (pid1 == 0) {
        // 子进程 A:写入者
        dup2(pipefd[1], 1); // 标准输出重定向到管道写端
        close(pipefd[0]);               // 关闭不用的读端
        close(pipefd[1]);               // 已重定向,原始描述符可关

        printf("abcd"); //输出变成向新建的管道写入
        exit(1);
    }

    pid_t pid2 = fork();

    if (pid2 == 0) {
        // 子进程 B:读取者
        dup2(pipefd[0], 0);  // 标准输入重定向到管道读端
        close(pipefd[1]);               // 关闭不用的写端
        close(pipefd[0]);
        char buf[5];
        scanf("%s",buf); //从终端读变成了从管道写入读
        printf("%s",buf);
        exit(1);
    }

    // 父进程关闭两端,等待
    close(pipefd[0]);
    close(pipefd[1]);

    wait(NULL);
    wait(NULL);

    return 0;
}

运行一下:

abcd

完美,注释掉第二个子进程的printf再试一下

什么输出也没有,所以可以确认时第二个子进程完成的输出