1. fork后内核做什么?
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构拷贝子进程
- 将子进程添加到系统进程列表中
- fork返回开始调度器调度
2. fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了上限
3. 进程退出场景
- 代码运行完毕,结果正确
- 代码运行完毕,结果不正确
- 代码异常终止
4. 查看进程退出码
echo $? 只会保留最近一次执行的进程的退出码
5. 进程退出常用方法
- main函数中return返回
- 调用exit()函数
- 调用_exit()函数
6. exit()函数和_exit()函数的区别
- exit()函数执行用户定义的清理函数
- 关闭所有打开的流,所有缓存器均被写入
- 调用_exit()函数
7. 进程的等待必要性
- 僵尸进程可能会造成内存泄漏
- 回收子进程资源,获取子进程退出信息
8. 进程等待方法
wait()函数方法:成功返回等待进程PID,失败则返回-1;参数不关心则可以设置NULL
waitpid()函数方法:成功返回子进程PID,如果设置了WNOHANG,而调用中waitpid已经收集返回0,调用出错返回-1;参数:PID=-1等待任一一个子进程,PID>0等待其进程PID与子进程相等的PID;status: WIFEXITED:查看进程是否退出,WEXITSTATUS:查看进程退出码:options:若指定PID的子进程没结束,返回0;结束返回子进程PID
status:是int*类型,指向的是一个int大小的空间,32个比特位,其中从右向左数,第7-15这八个比特位是退出状态,第0-6这7个比特位是终止信号,第7这个比特位是core dump标志位
怎么获取进程的退出码和终止信号呢?
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { //子进程 int count = 0; while(1) { if(count == 5) { break; } printf("我是子进程,PID:%d,PPPID:%d\n", getpid(), getppid()); count++; sleep(1); } exit(2); } //父进程 int status = 0; pid_t father_id = waitpid(id, &status, 0); printf("我是父进程,PID:%d,PPID:%d,father_id:%d,子进程退出码:%d,子进程终止信号:%d\n", \ getpid(), getppid(), father_id, (status >> 8) & 0xFF, status & 0x7F); return 0; }
父进程在wait的时候,如果子进程没有退出,父进程是在干什么呢?
在子进程没有的时候,父进程只有一直在调用waitpid进程等待,这种等待就是阻塞等待。
如果父进程不想在waitpid处卡住呢,而是去做做别的事情呢,这时怎么来解决这个问题?
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { //子进程 int count = 0; while(1) { if(count == 5) { break; } printf("我是子进程,PID:%d,PPPID:%d\n", getpid(), getppid()); count++; sleep(1); } exit(2); } //父进程 while(1) { int status = 0; pid_t father_id = waitpid(id, &status, WNOHANG); if(father_id < 0) { printf("err\n"); exit(-1); } else if(father_id == 0) { printf("子进程没有退出\n"); sleep(1); continue; } else { printf("我是父进程,PID:%d,PPID:%d,father_id:%d,子进程退出码:%d,子进程终止信号:%d\n", \ getpid(), getppid(), father_id, (status >> 8) & 0xFF, status & 0x7F); break; } return 0; } }
也可以用宏来获取退出码
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { //子进程 int count = 0; while(1) { if(count == 5) { break; } printf("我是子进程,PID:%d,PPPID:%d\n", getpid(), getppid()); count++; sleep(1); } exit(2); } //父进程 while(1) { int status = 0; pid_t father_id = waitpid(id, &status, WNOHANG); if(father_id < 0) { printf("err\n"); exit(-1); } else if(father_id == 0) { printf("子进程没有退出\n"); sleep(1); continue; } else { if(WIFEXITED(status)) //收到信号 { printf("等待成功,退出码是:%d\n", WEXITSTATUS(status)); } else { printf("等待成功,信号是:%d\n", status & 0x7F); } break; } return 0; } }
9. 进程程序替换
9.1 现象
[jyh@VM-12-12-centos study8]$ ll total 24 -rw-rw-r-- 1 jyh jyh 152 Mar 7 16:55 Makefile -rw-rw-r-- 1 jyh jyh 2045 Mar 7 16:43 proc.c -rwxrwxr-x 1 jyh jyh 8416 Mar 7 16:56 procReplace -rw-rw-r-- 1 jyh jyh 271 Mar 7 16:56 procReplace.c [jyh@VM-12-12-centos study8]$ cat procReplace.c #include <stdio.h> #include <unistd.h> int main() { printf("begin......\n"); printf("begin......\n"); printf("begin......\n"); execl("/bin/ls", "ls", "-a", "-l", NULL); printf("end.....\n"); printf("end.....\n"); printf("end.....\n"); return 0; } [jyh@VM-12-12-centos study8]$ ./procReplace begin...... begin...... begin...... total 32 drwxrwxr-x 2 jyh jyh 4096 Mar 7 16:56 . drwxrwxr-x 15 jyh jyh 4096 Mar 7 15:42 .. -rw-rw-r-- 1 jyh jyh 152 Mar 7 16:55 Makefile -rw-rw-r-- 1 jyh jyh 2045 Mar 7 16:43 proc.c -rwxrwxr-x 1 jyh jyh 8416 Mar 7 16:56 procReplace -rw-rw-r-- 1 jyh jyh 271 Mar 7 16:56 procReplace.c
我们发现上述代码中执行procReplace.c这个进程的时候,然后调用execl来执行另外一个程序时,procRepalce.c进程被替换了,并没有执行完execl再来执行procReplace.c程序,这就是程序替换。
9.2 原理
进程有对应的PCB,最后数据和代码别加载到内存中,调用execl函数时,该进程的代码和数据完全被新程序替换,执行新程序。进程的程序替换并没有创建新的进程,调用execl函数前后该进程的PID并没有变化。
[jyh@VM-12-12-centos study8]$ ll total 24 -rw-rw-r-- 1 jyh jyh 152 Mar 7 16:55 Makefile -rw-rw-r-- 1 jyh jyh 2045 Mar 7 16:43 proc.c -rwxrwxr-x 1 jyh jyh 8624 Mar 7 17:21 procReplace -rw-rw-r-- 1 jyh jyh 578 Mar 7 17:21 procReplace.c [jyh@VM-12-12-centos study8]$ cat procReplace.c #include <stdio.h> #include <unistd.h> #include <sys/wait.h> int main() { pid_t id = fork(); if(id == 0) { printf("我是子进程,我的PID:%d\n", getpid()); execl("/bin/ls","ls", "-a", "-l"); } sleep(5); printf("我是父进程,我的PID:%d\n", getpid()); waitpid(id, NULL, 0); return 0; } [jyh@VM-12-12-centos study8]$ ./procReplace 我是子进程,我的PID:8085 total 32 drwxrwxr-x 2 jyh jyh 4096 Mar 7 17:21 . drwxrwxr-x 15 jyh jyh 4096 Mar 7 15:42 .. -rw-rw-r-- 1 jyh jyh 152 Mar 7 16:55 Makefile -rw-rw-r-- 1 jyh jyh 2045 Mar 7 16:43 proc.c -rwxrwxr-x 1 jyh jyh 8624 Mar 7 17:21 procReplace -rw-rw-r-- 1 jyh jyh 578 Mar 7 17:21 procReplace.c 我是父进程,我的PID:8084 [jyh@VM-12-12-centos study8]$
上述观察到:子进程中调用execl函数并不会影响父进程,说明操作系统在调用execl时对代码和数据完成了写时拷贝。这里的写时拷贝是拷贝的代码,说明写时拷贝可以发生在代码区。
9.3 接口
- execl
int execl(const char* path, const char* arg, ...); //path:路径 //arg:参数 //最后参数列表必须以NULL结尾
实例
#include <stdio.h> #include <unistd.h> int main() { execl("/bin/pwd", "pwd", NULL); return 0; }
- execv
int execv(const char* path, char* const argv[]); //path:路径 //argv:字符串数组 //argv[]中必须以NULL结尾
实例
#include <stdio.h> #include <unistd.h> int main() { char* const argv[] = {"ls", "-a", "-l", NULL}; execv("/bin/ls", argv); return 0; }
- execlp
int execlp(const char* file, const char* arg, ...); //file:文件 //arg:参数 //参数列表必须以NULL结尾 //作用:系统自动在环境变量PATH中进行查找file按照参数执行
实例
#include <stdio.h> #include <unistd.h> int main() { execlp("ls", "ls", "-a", "-l", NULL); return 0; } //这里的两个ls不一样,第一个ls是执行文件名,第二个ls及以后是怎样执行的参数
- execvp
int execvp(const char* file, char* const argv[], ...);
实例
[jyh@VM-12-12-centos study9]$ tree . |-- exec | |-- Makefile | |-- otherTest | `-- otherTest.cc |-- Makefile |-- mytest `-- test.c 1 directory, 6 files [jyh@VM-12-12-centos study9]$ cat test.c #include <stdio.h> #include <unistd.h> int main() { char* const myenv[] = {"MYENV=study9/test.c", NULL}; execle("./exec/otherTest", "otherTest", NULL, myenv); return 0; } [jyh@VM-12-12-centos study9]$ cat ./exec/otherTest.cc #include <iostream> #include <unistd.h> #include <stdlib.h> using namespace std; int main() { for(int i = 0; i < 3; ++i) { cout << "我是exec路径下的程序,我的PID是: " << getpid() << " " << "MYENV:"<< (getenv("MYENV") == NULL ? "NULL" : getenv("MYENV")) << endl; sleep(1); } return 0; } [jyh@VM-12-12-centos study9]$ cat Makefile mytest:test.c gcc -o $@ $^ .PHONY:clean clean: rm -f mytest [jyh@VM-12-12-centos study9]$ cat ./exec/Makefile otherTest:otherTest.cc g++ -o $@ $^ .PHONY:clean clean: rm -f otherTest [jyh@VM-12-12-centos study9]$ ./mytest 我是exec路径下的程序,我的PID是: 6359 MYENV:study9/test.c 我是exec路径下的程序,我的PID是: 6359 MYENV:study9/test.c 我是exec路径下的程序,我的PID是: 6359 MYENV:study9/test.c [jyh@VM-12-12-centos study9]$
这里就可以掌握使用了。但是看下面的问题:
[jyh@VM-12-12-centos study9]$ clear [jyh@VM-12-12-centos study9]$ tree . |-- exec | |-- Makefile | |-- otherTest | `-- otherTest.cc |-- Makefile |-- mytest `-- test.c 1 directory, 6 files [jyh@VM-12-12-centos study9]$ cd exec/ [jyh@VM-12-12-centos exec]$ cat otherTest.cc #include <iostream> #include <unistd.h> #include <stdlib.h> using namespace std; int main() { for(int i = 0; i < 3; ++i) { cout << "------------------------------------------------------" << endl; cout << "我是exec路径下的程序,我的PID是: " << getpid() << endl; cout << "MYENV:"<< (getenv("MYENV") == NULL ? "NULL" : getenv("MYENV")) << endl; cout << "PATH:"<< (getenv("PATH") == NULL ? "NULL" : getenv("PATH")) << endl; cout << "------------------------------------------------------" << endl; sleep(1); } return 0; } [jyh@VM-12-12-centos exec]$ ./otherTest ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 8867 MYENV:NULL PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/jyh/.local/bin:/home/jyh/bin ------------------------------------------------------ ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 8867 MYENV:NULL PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/jyh/.local/bin:/home/jyh/bin ------------------------------------------------------ ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 8867 MYENV:NULL PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/jyh/.local/bin:/home/jyh/bin ------------------------------------------------------ [jyh@VM-12-12-centos exec]$ cd .. [jyh@VM-12-12-centos study9]$ pwd /home/jyh/linux_-stu/study9 [jyh@VM-12-12-centos study9]$ cat test.c #include <stdio.h> #include <unistd.h> int main() { char* const myenv[] = {"MYENV=study9/test.c", NULL}; execle("./exec/otherTest", "otherTest", NULL, myenv); return 0; } [jyh@VM-12-12-centos study9]$ ./mytest ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 8910 MYENV:study9/test.c PATH:NULL ------------------------------------------------------ ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 8910 MYENV:study9/test.c PATH:NULL ------------------------------------------------------ ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 8910 MYENV:study9/test.c PATH:NULL ------------------------------------------------------[jyh@VM-12-12-centos study9]$
现象:当本程序中设定自定义环境变量,另外的程序中有系统环境变量,此时运行本程序时,另外的程序中的系统环境变量为NULL。说明了:自定义环境变量是覆盖式传入,覆盖了系统的环境变量,所以导致这里系统环境变量为NULL。这里怎么解决呢?
int putenv(char* string); //作用:改变或者添加一个环境变量
[jyh@VM-12-12-centos study9]$ tree . |-- exec | |-- Makefile | |-- otherTest | `-- otherTest.cc |-- Makefile |-- mytest `-- test.c 1 directory, 6 files [jyh@VM-12-12-centos study9]$ cat ./exec/otherTest.cc #include <iostream> #include <unistd.h> #include <stdlib.h> using namespace std; int main() { for(int i = 0; i < 3; ++i) { cout << "------------------------------------------------------" << endl; cout << "我是exec路径下的程序,我的PID是: " << getpid() << endl; cout << "MYENV:"<< (getenv("MYENV") == NULL ? "NULL" : getenv("MYENV")) << endl; cout << "PATH:"<< (getenv("PATH") == NULL ? "NULL" : getenv("PATH")) << endl; cout << "------------------------------------------------------" << endl; sleep(1); } return 0; } [jyh@VM-12-12-centos study9]$ cat test.c #include <stdio.h> #include <unistd.h> #include <stdlib.h> int main() { extern char** environ; char* const myenv[] = {"MYENV=study9/test.c", NULL}; putenv("MYENV=study9/test.c"); execle("./exec/otherTest", "otherTest", NULL, environ); return 0; } [jyh@VM-12-12-centos study9]$ ./mytest ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 14363 MYENV:study9/test.c PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/jyh/.local/bin:/home/jyh/bin ------------------------------------------------------ ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 14363 MYENV:study9/test.c PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/jyh/.local/bin:/home/jyh/bin ------------------------------------------------------ ------------------------------------------------------ 我是exec路径下的程序,我的PID是: 14363 MYENV:study9/test.c PATH:/usr/local/bin:/usr/bin:/usr/local/sbin:/usr/sbin:/home/jyh/.local/bin:/home/jyh/bin ------------------------------------------------------ [jyh@VM-12-12-centos study9]$
这里考虑一个问题:环境变量具有全局属性,可以被子进程继承下去,这里怎么做到的呢?
因为所有的指令都是bash的子进程,而bash执行指令都可以使用execl来执行,我们要把bash的环境变量交给子进程,子进程只是需要调用execle,再把对应的bash的环境变量作为参数即可被子进程继承下去。
- execvpe
int execvpe(const char* file, char* const argv[], char* const envp[]);
- execve
int execve(const char* filename, char* const argv[], char* const envp[]);
10. 编写极简shell
[jyh@VM-12-12-centos demo]$ ll total 8 -rw-rw-r-- 1 jyh jyh 74 Mar 8 17:10 Makefile -rw-rw-r-- 1 jyh jyh 1716 Mar 8 17:31 mybash.c [jyh@VM-12-12-centos demo]$ cat mybash.c #include <stdlib.h> #include <stdio.h> #include <unistd.h> #include <string.h> #include <assert.h> #include <sys/wait.h> #include <sys/types.h> //模拟实现极简bash命令行 #define COMMANDMAXSIZE 1024 #define ARGVMAXSIZE 64 int split(char* command_string, char* argv[]) { assert(command_string != NULL && argv != NULL); argv[0] = strtok(command_string, " "); if(argv[0] == NULL) { return -1; } int i = 1; while((argv[i++] = strtok(NULL, " "))); //while(1) //{ // argv[i] = strtok(NULL, " "); // if(argv[i] == NULL) // { // break; // } // ++i; //} return 0; } void debugOut(char* argv[]) { int i = 0; while(argv[i] != NULL) { printf("%d->%s\n", i, argv[i]); ++i; } //for(int i = 0; argv[i] != NULL; ++i) //{ // printf("%d->%s\n", i, argv[i]); //} } int main() { while(1) { char command_string[COMMANDMAXSIZE] = {0}; printf("[name@my_root currrent_path]# "); fflush(stdout); char* s = fgets(command_string, sizeof(command_string), stdin); assert(s != NULL); (void)s; //保证release版本时,因为去掉assert()导致ret未被使用,而带来的编译告警 command_string[strlen(command_string) - 1] = '\0'; //去掉回车导致的换行 char* argv[ARGVMAXSIZE] = {NULL}; int ret = split(command_string, argv); //命令字符串切割 if(ret != 0) { continue; } //debugOut(argv); pid_t id = fork(); assert(id >= 0); if(id == 0) //子进程 { execvp(argv[0], argv); exit(0); } //父进程 int status = 0; waitpid(id, &status, 0); //子进程去执行对应的命令,父进程等待子进程 } return 0; } [jyh@VM-12-12-centos demo]$ cat Makefile mybash:mybash.c gcc -o $@ $^ -std=c99 .PHONY:clean clean: rm -f mybash [jyh@VM-12-12-centos demo]$