0711:
01:什么是孤儿进程,什么是僵尸进程,他们对操作系统有什么影响?
答案:
孤儿进程:当父进程退出时,它的子进程将成为孤儿进程,孤儿进程将由 init 进程 (1号进程) 收养,并最终由 init 进程完成对它们的终止状态手机工作。
僵尸进程:子进程终止,但父进程并没有调用 wait 或 waitpid 收集子进程的终止状态信息,这样的子进程称为僵尸进程。
unix提供了一种机制可以保证只要父进程想知道子进程终止状态信息, 就可以得到。这种机制就是: 在每个进程退出的时候,内核释放该进程所有的资源,包括打开的文件,占用的内存等。 但是仍然为其保留一定的信息(包括pid, 终止状态和运行时间等)。直到父进程通过wait /waitpid来取时才释放。 但这样就导致了问题,如果进程不调用wait / waitpid的话,那么保留的那段信息就不会释放,其进程号就会一直被占用,但是系统所能使用的进程号是有限的,如果大量的产生僵尸进程,将因为没有可用的进程号而导致系统不能产生新的进程. 此即为僵尸进程的危害,应当避免。
孤儿进程会由 init 进程收养,因此不会产生太大的影响。
02: 下面程序分别会输出多少个 a ? 为什么?
int main(int argc, char* argv[])
{
for (int i = 0; i < 3; i++) {
printf("a\n");
fork();
}
return 0;
}
int main(int argc, char* argv[])
{
for (int i = 0; i < 3; i++) {
printf("a");
fork();
}
return 0;
}
03:exit()函数的原理是什么?它会执行哪些步骤?请写程序验证你的答案。
解答:
1.调用退出处理程序 (通过 atexit() 和 on_exit()注册的函数),其执行顺序与注册顺序相反。
2.刷新 stdio 流缓冲区。
3.将 status 作为参数,调用_exit()系统调用。
#include <func.h>
void onExitFunc1(int status,void* arg){
printf("onExitFunction call :stat-%d, arg=%ld",status,(long)arg);
}
void function1(void){
printf("但是咸粽子才是最强的");
}
void function2(void){
printf("豆腐脑配什么都好吃,");
}
int main()
{
on_exit(onExitFunc1,(void*)10);
atexit(function1);
atexit(function2);
on_exit(onExitFunc1,(void*)20);
printf("纷争开始了\n");
sleep(3);
return 0;
}
04:假定我们可以修改一个程序的源代码,我们如何在一个指定的时间获取进程的 core 文件 (当时程序执行的状态),同时让该进程可以继续执行?
int main(int argc, char* argv[])
{
// 执行一些代码
// ...
// 获取core文件,请在这里填写你的代码:
// 执行后续逻辑
// ...
return 0;
}
解答:
答案:
int main(int argc, char* argv[]) { // 执行一些代码 // ... // 获取core文件,请在这里填写你的代码: switch (fork()) { case -1: error(1, errno, "fork"); case 0: // child abort(); default: // parent break; } // 执行后续逻辑 // ... return 0; }
0712:
01:使用有名管道实现远程拷贝的功能. (一个进程读文件,然后通过管道输送给另一个进程, 另一个进程写文件)。
int main(int argc, char* argv[])
{
// ./send file
}
答案:
// send.c #include <func.h> #define MAXSIZE 4096 int main(int argc, char* argv[]) { // ./send file if (argc != 2) { error(1, 0, "Usage: %s file", argv[0]); } // 1. 打开文件描述符 int fileFd = open(argv[1], O_RDONLY); if (fileFd == -1) { error(1, errno, "open %s", argv[1]); } // fifo 是事先用 mkfifo 命令创建的有名管道 int fifoFd = open("fifo", O_WRONLY); if (fifoFd == -1) { error(1, errno, "open fifo"); } // 2. 传输文件 char buf[MAXSIZE]; int nbytes; while ((nbytes = read(fileFd, buf, MAXSIZE)) > 0) { write(fifoFd, buf, nbytes); } // 关闭文件描述符 close(fileFd); close(fifoFd); return 0; } // recv.c #include <func.h> #define MAXSIZE 4096 int main(int argc, char* argv[]) { // ./recv file if (argc != 2) { error(1, 0, "Usage: %s file", argv[0]); } // 1. 打开文件描述符 int fileFd = open(argv[1], O_WRONLY | O_CREAT | O_TRUNC, 0666); if (fileFd == -1) { error(1, errno, "open %s", argv[1]); } // fifo 是事先用 mkfifo 命令创建的有名管道 int fifoFd = open("fifo", O_RDONLY); if (fifoFd == -1) { error(1, errno, "open fifo"); } // 2. 保存文件 char buf[MAXSIZE]; int nbytes; while ((nbytes = read(fifoFd, buf, MAXSIZE)) > 0) { write(fileFd, buf, nbytes); } // 3. 关闭文件描述符 close(fileFd); close(fifoFd); return 0; }
02:(a) 用自己的话阐述一下 select 的原理。
(b) select 的缺陷有哪些?
(c) 用 select 实现点对点聊天系统。(用两个有名管道进行进程间通信,并且当一端强制关闭时,另一端不会出现死循环)
答案:
(a) select 系统调用的工作原理主要涉及以下几个步骤:
设置监视的文件描述符集合:调用 select 时,程序会指定三个文件描述符集合(读、写、异常)和一个超时时间。这些集合指示 select 要监视哪些文件描述符,以及需要监视哪类事件(读就绪、写就绪或异常条件)。
阻塞等待事件发生:一旦调用 select,调用的进程或线程将被阻塞,直到以下几种情况之一发生:
至少一个监视的文件描述符变为就绪状态。
发生一个异常条件。
超时时间到达(如果设置了超时时间)。
检查结果:当 select 返回后,它会更新传入的文件描述符集合,反映哪些文件描述符是就绪的。处理就绪的文件描述符:程序可以根据 select 返回的结果对就绪的文件描述符进行相应的 I/O 操作。
(b) select 主要有两个缺陷:
1. 监听的文件描述符个数有限,通常最大为 1024 个。
2. 在处理大量文件描述符时,select 可能会出现性能问题。因为每次调用都需要在用户空间和内核空间复制整个文件描述符集合。并且,当 select 返回后,用户需要遍历整个文件描述符集合,来检查哪些文件描述符是就绪的。
(c) // p1.c #include <func.h> #define MAXLINE 128 #define MAXSIZE 128 int main(int argc, char* argv[]) { char line[MAXLINE] = ""; char buf[MAXSIZE] = ""; int fd1 = open("pipe111", O_RDONLY); int fd2 = open("pipe222", O_WRONLY); printf("pipes open\n"); fd_set mainfds; FD_ZERO(&mainfds); FD_SET(STDIN_FILENO, &mainfds); int maxfd = STDIN_FILENO; FD_SET(fd1, &mainfds); if (fd1 > maxfd) { maxfd = fd1; } for(;;) { fd_set readfds = mainfds; // 结构体的复制 int nfds = select(maxfd + 1, &readfds, NULL, NULL, NULL); if (nfds == -1) { error(1, errno, "select"); } // 测试哪些文件描述符就绪了 if (FD_ISSET(STDIN_FILENO, &readfds)) { fgets(line, MAXLINE, stdin); write(fd2, line, strlen(line) + 1); } if (FD_ISSET(fd1, &readfds)) { int nbytes = read(fd1, buf, sizeof(buf)); switch (nbytes) { case -1: error(1, errno, "read"); case 0: // 管道的写端关闭 goto end; default: // 将读取的数据打印到STDOUT printf("from p2: %s", buf); break; } } } end: close(fd1); close(fd2); return 0; } // p2.c #include <func.h> #define MAXLINE 128 #define MAXSIZE 128 int main(int argc, char* argv[]) { char line[MAXLINE] = ""; char buf[MAXSIZE] = ""; int fd1 = open("pipe111", O_WRONLY); int fd2 = open("pipe222", O_RDONLY); printf("pipes open\n"); fd_set mainfds; FD_ZERO(&mainfds); FD_SET(STDIN_FILENO, &mainfds); int maxfd = STDIN_FILENO; FD_SET(fd2, &mainfds); if (fd2 > maxfd) { maxfd = fd2; } for(;;) { fd_set readfds = mainfds; // 结构体的复制 int nfds = select(maxfd + 1, &readfds, NULL, NULL, NULL); if (nfds == -1) { error(1, errno, "select"); } // 测试哪些文件描述符就绪了 if (FD_ISSET(STDIN_FILENO, &readfds)) { fgets(line, MAXLINE, stdin); write(fd1, line, strlen(line) + 1); } if (FD_ISSET(fd2, &readfds)) { int nbytes = read(fd2, buf, sizeof(buf)); switch (nbytes) { case -1: error(1, errno, "read"); case 0: // 管道的写端关闭 goto end; default: // 将读取的数据打印到STDOUT printf("from p1: %s", buf); break; } } } end: close(fd1); close(fd2); return 0; }
03:请用 pipe 实现父子进程的全双工通信。
要求:父进程先读子进程的消息,然后给子进程发送消息;子进程先给父进程发送消息,然后再读取父进程的消息。
答案:
#include <func.h> #define MAXSIZE 128 int main(void) { int pipe_fd1[2], pipe_fd2[2]; // 创建两个管道 if (pipe(pipe_fd1) == -1 || pipe(pipe_fd2) == -1) { error(1, errno, "pipe"); } char buf[MAXSIZE]; pid_t pid = fork(); if (pid == -1) { error(1, errno, "fork"); } if (pid == 0) { // 子进程 close(pipe_fd1[0]); // 关闭第一个管道的读端 close(pipe_fd2[1]); // 关闭第二个管道的写端 // 向父进程发送消息 const char* message = "I'm a baby"; write(pipe_fd1[1], message, strlen(message) + 1); // 1 for '\0' // 从父进程读取消息 read(pipe_fd2[0], buf, sizeof(buf)); printf("From parent: %s\n", buf); close(pipe_fd1[1]); close(pipe_fd2[0]); } else { // 父进程 close(pipe_fd1[1]); // 关闭第一个管道的写端 close(pipe_fd2[0]); // 关闭第二个管道的读端 // 从子进程读取消息 read(pipe_fd1[0], buf, sizeof(buf)); printf("From child: %s\n", buf); // 向子进程发送消息 const char* message = "Who's your daddy?"; write(pipe_fd2[1], message, strlen(message) + 1); // 1 for '\0' close(pipe_fd1[0]); close(pipe_fd2[1]); } return 0; }
04:(拓展题) 请实现一个简易的 shell
int main(int argc, char* argv[])
{
for(;;) {
// 读取用户输入的命令 cmd
// 如果cmd为exit,终止进程
// 创建子进程,让子进程执行命令
// 父进程等待子进程结束,并打印子进程的终止状态信息。
}
}
答案:
#include <func.h> #define MAXLINE 1024 // 最大输入长度 #define MAXARGS 64 // 最大参数数量 // 解析命令行输入,分离命令和参数 void parseInput(char* input, char* args[]) { int i = 0; args[i] = strtok(input, " \n"); while (args[i] != NULL) { i++; args[i] = strtok(NULL, " \n"); } } void print_wstatus(int status) { if (WIFEXITED(status)) { int exit_code = WEXITSTATUS(status); printf("exit code: %d\n", exit_code); } else if (WIFSIGNALED(status)) { int signo = WTERMSIG(status); printf("signo: %d", signo); #ifdef WCOREDUMP if (WCOREDUMP(status)) { printf(" (core dump)"); } #endif printf("\n"); } } int main() { char input[MAXLINE]; // 存储输入的命令行 char* args[MAXARGS]; // 存储命令行参数 for(;;) { printf("myshell> "); // 提示符 fgets(input, MAXLINE, stdin); // 从stdin读取命令 if (strcmp(input, "exit\n") == 0) { // 检查是否为exit命令 break; // 退出循环 } parseInput(input, args); // 解析输入的命令 switch (fork()) { case -1: error(1, errno, "fork"); case 0: // 子进程执行命令 if (execvp(args[0], args) == -1) { error(1, errno, "execvp"); } default: // 父进程等待子进程结束 int wstatus; pid_t pid = wait(&wstatus); printf("\n%d terminated. ", pid); print_wstatus(wstatus); } } return 0; }
0713:
01:(1) 信号是一种事件通知机制,产生信号的事件源有哪些?
(2) 常见的信号有哪些?它们的默认行为是什么?
解答:
(1)
硬件:硬件检测到一个错误并通知内核,随即再由内核发送相应的信号给相关进程。如:
算术异常(如被 0整除),SIGFPE。
执行非法的指令,SIGILL。
访问非法的内存,SIGSEGV
用户:用户键入了能够产生信号的终端特殊字符。
中断字符(ctr1+C),SIGINT
暂停字符(ctr1+z),SIGTSTP
退出字符(ctr1+\),SIGQUIT
软件(内核或进程):内核或进程(可以是该进程,也可以是其它进程)发生了某些事件。如:
子进程死亡,内核会给父进程发送 SIGCHLD信号。
管道的读端关闭了 (这样的管道叫做 broken pipe),再向管道写数据;内核会向写进程发送SIGPIPE 信号。
调用 abort()函数,内核会向调用进程发送SIGABRT信号
(2)
SIGABRT:进程异常终止信号,通常由调用 abort() 函数产生。
SIGALRM:定时器超时信号,通常用来实现定时功能。
SIGBUS:非法地址访问信号,通常由内存对齐错误引起。
SIGCHLD:子进程状态变更信号,父进程在子进程退出时接收。
SIGCONT:继续执行信号,用于从暂停状态中恢复进程。
SIGFPE:浮点异常信号,通常由数学运算错误(如除以零)引起。
SIGHUP:终端挂断信号,通常在终端断开连接时发送给进程。
SIGILL:非法指令信号,通常由执行非法机器指令引起。
SIGINT:终端中断信号,通常由用户在终端上按下 Ctrl+C 产生。
SIGIO:异步 I/O 事件完成信号,通常用于异步 I/O 操作。
SIGKILL:强制终止信号,无法被捕获或忽略,用于强制结束进程。
SIGPIPE:管道破裂信号,当进程向已关闭的管道写入时发送。
SIGQUIT:终端退出信号,通常由用户在终端上按下 Ctrl+\ 产生。
SIGSEGV:段错误信号,通常由内存访问越界或无效指针引起。
SIGSTOP:停止信号,用于暂停进程的执行。
SIGTSTP:终端停止信号,通常由用户在终端上按下 Ctrl+Z 产生。
SIGTERM:终止信号,通常用于请求进程正常终止。
02:(1) 完成下面程序:然后在终端分别发送 SIGINT, SIGQUIT 和 SIGTSTP,看程序会如何反应。(2) 实现程序:可以给多个指定的进程发送同一个信号。使用这个程序,给上面的程序发送信号。
int main(int argc, int argv) {
// 1. 分别捕获 SIGINT, SIGQUIT 和 SIGTSTP
for (;;) {
sleep(5);
}
}
int main(int argc, char* argv[]) {
// ./test_kill signo pid...
}
解答:
#include <func.h>
void handler(int signo){
switch (signo){
case SIGINT:
printf(" caught SIGINT\n");
break;
case SIGTSTP:
printf(" caught SIGTSTP\n");
break;
default :
printf("Ukonown signal\n");
}
}
int main(int argc,char* argv[])
{
//zhuce signal function
sighandler_t oldhandler=signal(SIGINT,handler);
if(oldhandler==SIG_ERR){
error(1,errno,"singal SIGINT");
}
oldhandler=signal(SIGTSTP,handler);
if(oldhandler==SIG_ERR){
error(1,errno,"singal SIGTSTP");
}
//run
printf("programing pid=%d running...\n",getpid());
for(;;){
}
return 0;
}
#include <func.h>
int main(int argc,char* argv[])
{
//./t_kill signo pid
if(argc<3){
error(1,0,"Usage :%s signo pid ...",argv[0]);
}
int signo; //要发送的信号
sscanf(argv[1],"%d",&signo);
for(int i=2;i<argc;i++){
pid_t pid;
sscanf(argv[i],"%d",&pid);//从argv[i]中读取pid
if(kill(pid,signo)==-1){
error(0,errno,"kill(%d,%d)",pid,signo);
}
}
return 0;
}