进程创建
在linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程。
#include <unistd.h>pid_t fork(void);返回值:子进程中返回0,父进程返回子进程id,出错返回-1
进程调用fork,当控制转移到内核中的fork代码后,内核做:
进程:内核的相关管理数据结构(task_struct+mm_struct+页表)+代码(共享)和数据(写时拷贝)
一个父进程希望复制自己,使父子进程同时执行不同的代码段。例如,父进程等待客户端请求,生成子进程来处理请求一个进程要执行一个不同的程序。例如子进程从fork返回后,调用exec函数
系统中有太多的进程实际用户的进程数超过了限制
进程终止
终止是在做什么?
释放曾经的代码和数据所占据的空间
释放内核数据结构
(注意:僵尸进程不会释放内核数据结构,只释放代码和数据所占据的空间)
进程退出场景
代码运行完毕,结果正确代码运行完毕,结果不正确代码异常终止
进程常见退出方法
正常终止
(可以通过 echo $? 查看最近一个子进程退出码)1. 从main返回 (return)2. 调用exit3. _exit
父进程bash获取到最近一个子进程退出的退出码
告诉父进程,子进程的任务完成的怎么样,知道子进程(成功,失败:原因)
0:代表成功
!0:代表失败,[0-255]:不同的失败原因
退出码可以用系统的,也可以自定义退出码
return
非main函数return,函数结束
我们平时所写的main函数return 0是我们默认我们的函数运行正确,实际上内部需要判断再进行返回给bash的
exit
我们的代码任意位置调用exit,都表示进程退出
参数:status 定义了进程的终止状态,父进程通过wait来获取该值
exit最后会调用_exit, 但在调用_exit之前,还做了其他工作:1. 执行用户通过 atexit或on_exit定义的清理函数。2. 关闭所有打开的流,所有的缓存数据均被写入3. 调用_exit![]()
_exit
void _exit(int status);
参数: status 定义了进程的终止状态,父进程通过 wait 来获取该值相比exit,_exit不会刷新缓冲区
异常终止
操作系统发现了你的进程做了不该做的事,OS杀了进程
一旦出现异常,退出码就没有意义了
进程出现异常,本质是:因为进程收到了OS发给进程的信号
段错误,OS提前终止了进程
我们可以看进程退出的时候,退出信号是多少,就可以判断我的进程为什么异常了
我们还可以手动让OS发信号终止进程
衡量一个进程的退出,父进程bash只需要两个数字: 退出码和退出信号
进程等待
进程等待的方法wait 方法
等待父进程中,任意一个子进程的退出![]()
子进程本身就是软件,父进程本质是在等待某种软件条件就绪
waitpid方法
pid_ t waitpid(pid_t pid, int *status, int options);返回值:当正常返回的时候 waitpid 返回收集到的子进程的进程 ID ;如果设置了选项 WNOHANG, 而调用中 waitpid 发现没有已退出的子进程可收集 , 则返回 0 ;如果调用中出错 , 则返回 -1, 这时 errno 会被设置成相应的值以指示错误所在;参数:pid:Pid=-1, 等待任一个子进程。与 wait 等效。Pid>0. 等待其进程 ID 与 pid 相等的子进程。status:WIFEXITED(status): 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)WEXITSTATUS(status): 若 WIFEXITED 非零,提取子进程退出码。(查看进程的退出码)options:WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回 0 ,不予以等待。若正常结束,则返回该子进程的ID 。
如果子进程已经退出,调用wait/waitpid时,wait/waitpid会立即返回,并且释放资源,获得子进程退 出信息。如果在任意时刻调用wait/waitpid,子进程存在且正常运行,则进程可能阻塞。如果不存在该子进程,则立即出错返回。![]()
ptions==0,阻塞等待:子进程没有等到时,会一直堵塞等待
ptions==WNOHANG,非阻塞等待:对子进程进行检测,子进程没退出,直接返回0
所以,ptions==0,返回值只有>0和<0两种情况
ptioons==WNOHANG,返回值有>0和<0和==0三种情况
非阻塞等待的时候+循环=非阻塞轮询
非阻塞等待允许父进程做一些其他事情
获取子进程status
![](https://i-blog.csdnimg.cn/blog_migrate/969ec3eb654155503bb4257a2a8e1109.png)
0000 0000 0 000 0000退出状态:[0,255]终止信号:7位表示够用(0~6)
子进程退出信息:进程退出码+退出信号
这样单独取到 进程退出码和退出信号
利用WIFEXITED(status)和WEXITSTATUS(status)判断和提取退出码
非堵塞等待测试
myprocss.c
#include <stdio.h> #include <unistd.h> #include <string.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> #include "task.h" typedef void(*func_t)(); #define N 3 func_t tasks[N] = {NULL}; void LoadTask() { tasks[0] = PrintLog; tasks[1] = Download; tasks[2] = MysqlDataSync; } void HandlerTask() { for(int i = 0; i < N; i++) { tasks[i](); // 回调方式 } } // fahter void DoOtherThing() { HandlerTask(); } void ChildRun() { //int *p = NULL; int cnt = 5; while(cnt) { printf("I am child process, pid: %d, ppid:%d, cnt: %d\n", getpid(), getppid(), cnt); sleep(1); cnt--; //*p = 100; } } int main() { printf("I am father, pid: %d, ppid:%d\n", getpid(), getppid()); pid_t id = fork(); if(id == 0) { // child ChildRun(); printf("child quit ...\n"); exit(123);//进程退出码没有返回时,默认为0 } LoadTask(); // father while(1) { int status = 0; pid_t rid = waitpid(id, &status, WNOHANG); // non block if(rid == 0) { usleep(100000); printf("child is running, father check next time!\n"); DoOtherThing(); } else if(rid > 0) { if(WIFEXITED(status)) { printf("child quit success, child exit code : %d\n", WEXITSTATUS(status)); } else { printf("child quit unnormal!\n"); } break; } else { printf("waitpid failed!\n"); break; } }
task.c
#include "task.h" void PrintLog() { printf("begin PrintLog...\n"); } void Download() { printf("begin Download...\n"); } void MysqlDataSync() { printf("begin MySQLDataSync...\n"); }
task.h
#include <stdio.h> void PrintLog(); void Download(); void MysqlDataSync();
进程程序替换
替换原理
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec*并不创建新进程,所以调用exec前后该进程的id并未改变
进程=内核数据结构+代码和数据只是进程的程序替换,没有创建新的进程,本质就是被替换进程的程序被加载到内存了
父进程的代码和数据本来是和子进程共享的,但由于进程程序替换,子进程后面替换的程序用的是替换的代码和数据,在物理内存上重新开辟一块空间放置新的代码和数据
替换函数
#include <unistd.h>int execl(const char *path, const char *arg, ...);int execlp(const char *file, const char *arg, ...);int execle(const char *path, const char *arg, ...,char *const envp[]);int execv(const char *path, char *const argv[]);int execvp(const char *file, char *const argv[]);int execvpe(const char *file, char *const argv[], char *const envp[]);int execve(const char *path, char *const argv[], char *const envp[]); 为系统接口
path:我们要执行的程序,需要带路径(怎么找到程序)
file:用户可以不传要执行的文件的路径(但文件名要传)
argu:在命令行中怎么执行你就怎么传参 最后一个要传NULL
argu[ ]: 用一个指针数组分别装命令字符串,最后一个空间传NULL,然后传给argu[ ]
envp[ ]:整体替换所有环境变量,可以自定义环境变量传入
*:
l :list 列表
v:vector
p:查找这个程序,系统会自动在环境变量PATH中进行查找
e:环境变量
exec*系列的函数执行完毕后,后续的代码不见了,因为被替换了
exec*函数的返回值不用关心,只要替换成功,就不会向后继续运行,只要继续运行了,一定是替换失败
exec*类似Linux上的加载函数
将代码改成多进程版
fork创建子进程,让子进程自己去替换,父进程wait
创建子进程,让子进程完成任务:1.让子进程执行父进程代码的一部分2.让子进程执行一个全新的程序
exec*会将传入的命令行参数或环境变量传给被替换进程的程序
mypragma.cc
#include <iostream> #include <unistd.h> using namespace std; int main(int argc, char *argv[], char *env[]) { int i = 0; for(; argv[i]; i++) { printf("argv[%d] : %s\n", i, argv[i]); } printf("-------------------------------\n"); for(i=0; env[i]; i++) { printf("env[%d] : %s\n", i, env[i]); } printf("-------------------------------\n"); cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; return 0; }
testexec.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { printf("testexec ... begin!\n"); pid_t id = fork(); if(id == 0) { char *const argv[] = { (char*)"mypragma" NULL }; char *const envp[] = { (char*)"HAHA=111111", (char*)"HEHE=222222", NULL }; printf("child pid: %d\n", getpid()); sleep(2); execvpe("./mypragma", argv, envp); exit(1); } // fahter int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { printf("father wait success, child exit code: %d\n", WEXITSTATUS(status)); } printf("testexec ... end!\n"); return 0; }
将父进程bash的环境变量传到被替换进程的程序中然后打印
mypragma.cc
#include <iostream> #include <unistd.h> using namespace std; int main(int argc, char *argv[], char *env[]) { int i = 0; for(; argv[i]; i++) { printf("argv[%d] : %s\n", i, argv[i]); } printf("-------------------------------\n"); for(i=0; env[i]; i++) { printf("env[%d] : %s\n", i, env[i]); } printf("-------------------------------\n"); cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; return 0; }
testexec.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { printf("testexec ... begin!\n"); pid_t id = fork(); if(id == 0) { // 我的父进程本身就有一批环境变量!!!, 从bash来 char *const argv[] = { (char*)"mypragma", (char*)"-a", (char*)"-b", NULL }; char *const envp[] = { (char*)"HAHA=111111", (char*)"HEHE=222222", NULL }; extern char**environ; printf("child pid: %d\n", getpid()); sleep(2); execvpe("./mypragma", argv, environ); exit(1); } // fahter int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { printf("father wait success, child exit code: %d\n", WEXITSTATUS(status)); } printf("testexec ... end!\n"); return 0; }
替换自己的程序
testexec.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { printf("testexec ... begin!\n"); pid_t id = fork(); if(id == 0) { sleep(2); execl("./mypragma", "mypragma", NULL); exit(1); } // fahter int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { printf("father wait success, child exit code: %d\n", WEXITSTATUS(status)); } printf("testexec ... end!\n"); return 0; }
替换自己的程序文件,由于我们的文件不在环境变量中,但可根据路径直接写文件名运行
我们还可以将路径写入环境变量或者自己定义环境变量去调用其他类型的替换函数
mypragma.cc
#include <iostream> #include <unistd.h> using namespace std; int main(int argc, char *argv[], char *env[]) { cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; cout << "hello C++, I am a C++ pragma!: " << getpid() << endl; return 0; }
Makefile
.PHONY:all all:testexec mypragma testexec:testexec.c gcc -o $@ $^ mypragma:mypragma.cc g++ -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f testexec mypragma
同时编译两个文件
替换其他程序
test.py
#!/usr/bin/python3 print("hello python") print("hello python") print("hello python") print("hello python")
test.sh
#!/usr/bin/bash cnt=0 while [ $cnt -le 10 ] do echo "hello shell, cnt: ${cnt}" let cnt++ done
testexec.c
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/wait.h> int main() { printf("testexec ... begin!\n"); pid_t id = fork(); if(id == 0) { printf("child pid: %d\n", getpid()); sleep(2); execl("/usr/bin/python3", "python3", "test.py", NULL); //execl("/usr/bin/bash", "bash", "test.sh", NULL); exit(1); } // fahter int status = 0; pid_t rid = waitpid(id, &status, 0); if(rid > 0) { printf("father wait success, child exit code: %d\n", WEXITSTATUS(status)); } printf("testexec ... end!\n"); return 0; }
这里是替换另一个进程的编译程序,然后再执行对某个文件的编译运行
putenv
系统接口函数
向环境变量表中添加环境变量
简易的shell
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)
// 为了方便,我就直接定义了
char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;
void Die()
{
exit(1);
}
const char *GetHome()
{
const char *home = getenv("HOME");
if(home == NULL) return "/";
return home;
}
const char *GetUserName()
{
const char *name = getenv("USER");
if(name == NULL) return "None";
return name;
}
const char *GetHostName()
{
const char *hostname = getenv("HOSTNAME");
if(hostname == NULL) return "None";
return hostname;
}
// 临时
const char *GetCwd()
{
const char *cwd = getenv("PWD");
if(cwd == NULL) return "None";
return cwd;
}
// commandline : output
void MakeCommandLineAndPrint()
{
char line[SIZE];
const char *username = GetUserName();
const char *hostname = GetHostName();
const char *cwd = GetCwd();
SkipPath(cwd);
snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
printf("%s", line);
fflush(stdout);
}
int GetUserCommand(char command[], size_t n)
{
char *s = fgets(command, n, stdin);
if(s == NULL) return -1;
command[strlen(command)-1] = ZERO;
return strlen(command);
}
void SplitCommand(char command[], size_t n)
{
(void)n;
// "ls -a -l -n" -> "ls" "-a" "-l" "-n"
gArgv[0] = strtok(command, SEP);
int index = 1;
while((gArgv[index++] = strtok(NULL, SEP))); // done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束
}
void ExecuteCommand()
{
pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
// child
execvp(gArgv[0], gArgv);
exit(errno);
}
else
{
// fahter
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
}
}
}
void Cd()
{
const char *path = gArgv[1];
if(path == NULL) path = GetHome();
// path 一定存在
chdir(path);//改变当前路径,系统接口
// 刷新环境变量
char temp[SIZE*2];
getcwd(temp, sizeof(temp));//获取当前的绝对路径,系统接口
snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
putenv(cwd); // OK //写入环境变量,系统接口
}
int CheckBuildin()
{
int yes = 0;
const char *enter_cmd = gArgv[0];
if(strcmp(enter_cmd, "cd") == 0)
{
yes = 1;
Cd();
}
else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{
yes = 1;
printf("%d\n", lastcode);
lastcode = 0;
}
return yes;
}
int main()
{
int quit = 0;
while(!quit)
{
// 1. 我们需要自己输出一个命令行
MakeCommandLineAndPrint();
// 2. 获取用户命令字符串
char usercommand[SIZE];
int n = GetUserCommand(usercommand, sizeof(usercommand));
if(n <= 0) return 1;
// 3. 命令行字符串分割.
SplitCommand(usercommand, sizeof(usercommand));
// 4. 检测命令是否是内建命令
n = CheckBuildin();
if(n) continue;
// 5. 执行命令
ExecuteCommand();
}
return 0;
}
cd,echo,export等为内建命令
然后 shell 读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程束。所以要写一个 shell ,需要循环以下过程 :1. 获取命令行2. 解析命令行3. 建立一个子进程( fork )4. 替换子进程( execvp )5. 父进程等待子进程退出( wait )