第五章课后题
第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再试一下
什么输出也没有,所以可以确认时第二个子进程完成的输出