赞
踩
man 2 fork
在上图中我们还可以看到返回类型是pid_t
,如果创建子进程失败,会返回小于0的数字,而如果创建子进程成功,该函数则会返回俩个值。它会给子进程返回0值,而给父进程返回子进程的pid(一个大于0的数),创建成功后我们可以对此进行使用if语句进行分流~~
下面简单验证再来验证一下:
#include <stdio.h> #include <stdlib.h> #include <unistd.h> int g_val = 100; int main() { pid_t pid = fork(); if(pid < 0) { printf("fork error!\n"); exit(-1); } else if(pid == 0) { //child g_val = 200; printf("This is Child! g_val = %d, p = %p\n",g_val,&g_val); } else { //parent sleep(1); printf("This is Parent! g_val = %d, p = %p\n",g_val,&g_val); } return 0; }
顾名思义,就是直接让进程终止,我们来了解一下:
#include <stdio.h>
int main()
{
return 1;
}
echo $?
1
int main()
{
int* p = NULL;
*p = 10;
return 0;
}
在编译运行的时候,出现了异常,提前退出了,操作系统发现了不该做的事情,OS杀死了进程
一旦出现了异常,退出码也就没有意义了,那么为什么出现了异常,是因为进程收到了OS发给进程的信号
在Linux中,可以使用kill -l
查看所表示的信号,可以看到0表示成功~,所以一般正常运行完成之后退出码就写成0,非0表示失败
Ctrl+C,信号终止等
exit退出函数和_exit退出函数:
return退出
衡量一个进程退出,我们只需要两个数字:退出码和退出信号
在父进程中,使用wait或waitpid接口来完成进程等待。
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id=fork(); if(id==0) { // child process int cnt=10; while(cnt) { printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--); sleep(1); } exit(0);// 子进程退出 } sleep(15); pid_t ret=wait(NULL); if(ret>0) { printf("wait success:%d!\n",ret); } sleep(5); return 0; }
waitpid:
参数:
这里就不再演示了~~
我们已经知道status是一个出参,由操作系统为其赋值,用户可以传递NULL值表示不关心,而如果传入参数,操作系统就会根据该参数,将子进程的退出信息反馈给父进程,由status最终被赋予的值来体现。
如何通过status来获取子进程的退出信息呢?
status是一个int类型的值,意味着它应该有32个比特位,但它又不能被当初普通的整形来看待,因为其高16位的值并不被使用,而只使用其低16个比特位:
退出码:(status >> 8) & 0xFF
低7位(检测子进程是否异常退出):status & 0x7F
结果为0则表示正常退出
不为0则说明是异常退出,因为有终止信号
core dump标志位:(status >> 7) & 0x1
结果为0则表示没有core dump产生
等于1则说明有core dump产生
#include <unistd.h> #include <stdlib.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> int main() { pid_t id=fork(); if(id==0) { // child process int cnt=10; while(cnt) { printf("我是子进程:%d,父进程:%d,cnt:%d\n",getpid(),getppid(),cnt--); sleep(1); } exit(6);// 子进程退出 } sleep(15); int status = 0; pid_t ret=waitpid(id,&status,0); if(ret>0) { printf("wait success:%d!\n",ret); printf("status:%d,退出码是%d,退出信号是%d\n",status,(status>>8)&0xFF,status&0x7F); } sleep(5); return 0; }
int* p = NULL;
*p = 100;
WIFEXITED(status):若子进程是正常终止,则返回结果为真,用于查看进程是否正常退出。
WEXITSTATUS(status):若进程正常终止,也就是进程终止信号为0,这时候会返回子进程的退出码。
下面我们可以写一个代码来演示一下
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> int main() { pid_t id = fork(); assert(id != -1); if(id == 0) { // child int cnt = 10; while(cnt) { printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--); sleep(1); } exit(10); } // 等待子进程 int status=0; int ret = waitpid(id,&status,0); if(ret > 0) { if(WIFEXITED(status)) { printf("child process exit normally,exit code:%d\n",WEXITSTATUS(status)); } else { printf("child process don't exit normally\n"); } // printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F); } return 0; }
当子进程还没有死的时候,也就是没有退出的时候,父进程调用的wait或waitpit需要等待子进程退出,系统调用接口也不返回,这段时间父进程什么都没做,就一直等待子进程退出,这样的等待方式,称之为阻塞式等待。
非阻塞式等待就是不停的检测子进程状态,每一次检测之后,系统调用立即返回,在waitpid中的第三个参数设置为WNOHANG,即为父进程非阻塞式等待。
如果等待的子进程状态没有发生变化,则waitpid会返回0值。多次非阻塞等待子进程,直到子进程退出,这样的等待方式又称之为轮询。如果等待的进程不是当前父进程的子进程,则waitpid会调用失败。
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> int main() { pid_t id = fork(); assert(id!=-1); if(id==0) { // child process int cnt=5; while(cnt) { printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--); sleep(3); } exit(10); } int status=0; while(1) { // WNOHANG是非阻塞等待,子进程没有退出,父进程检测一次之后,立即返回 pid_t ret=waitpid(id,&status,WNOHANG); if(ret == 0) { // waitpid调用成功,子进程没有退出 printf("Wait for success,but the child process is still running\n"); } else if(ret == id) { // waitpid调用成功,子进程退出 printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F); break; } else { // waitpid调用失败,例如等待了一个不属于该父进程的子进程 printf("The waitpid call failed\n"); break; } sleep(1); } return 0; }
非阻塞等待有一个好处就是,不会像阻塞式等待一样,父进程什么都做不了,而是在轮询期间,父进程还可以做其他的事情。
下面代码中,利用了回调函数的方式,来让父进程轮询等待子进程期间,还可以处理其他任务。
#include <stdio.h> #include <sys/types.h> #include <sys/wait.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <string.h> void task1() { printf("Process task1\n"); } void task2() { printf("Process task2\n"); } void task3() { printf("Process task3\n"); } typedef void (*func_t)();// 定义一个函数指针类型。 func_t Process_task[10]; void loadtask() { memset(Process_task,0, sizeof(Process_task)); Process_task[0]=task1; Process_task[1]=task2; Process_task[2]=task3; } int main() { pid_t id = fork(); assert(id!=-1); if(id==0) { // child process int cnt=5; while(cnt) { printf("child process running,pid:%d,ppid:%d,cnt:%d\n",getpid(),getppid(),cnt--); sleep(1); } exit(10); } loadtask();// 加载任务到函数指针数组里面。 int status=0; while(1) { pid_t ret=waitpid(id,&status,WNOHANG);// WNOHANG是非阻塞等待,子进程没有退出,父进程检测一次之后,立即返回 if(ret == 0) { // waitpid调用成功,子进程没有退出 printf("Wait for success,but the child process is still running\n"); for(int i=0; Process_task[i]!=NULL; i++) { Process_task[i]();// 回调函数的方式,让父进程在轮询期间,做其他事情 } } else if(ret == id) { // waitpid调用成功,子进程退出 printf("wait success,exit code:%d,signal number:%d\n",(status>>8)&0xFF,status & 0x7F); break; } else { // waitpid调用失败,例如等待了一个不属于该父进程的子进程 printf("The waitpid call failed\n"); break; } sleep(1); } return 0; }
让子进程执行父进程代码的一部分,也就是执行父进程对应的磁盘上的代码和数据的一部分。
让子进程加载磁盘上指定的程序到内存中,使其执行新的程序的代码和数据,这就是进程的程序替换。
接下来我们就介绍一些进程的程序的替换函数
int execl(const char* path,const char* arg,...);
man execl
例如:
#include <stdio.h>
#include <unistd.h>
int main()
{
// .c --> .exe --> load into memory --> process --> running
printf("The process is running...\n");
// 传参以NULL结尾,来表示传参结束
execl("/usr/bin/ls","ls","-a","-l","--color=auto",NULL);
printf("The process finishes running...\n");
return 0;
}
exec系列的函数只有在调用失败的时候才有返回值,这个返回值是-1,那为什么exec系列的函数没有调用成功时的返回值呢?
答案:没有必要,因为exec系列函数调用结束之后,代码就全都被替换了,就算给你返回值你也使用不了,因为代码全都替换为指定程序的代码了,所以只要exec系列函数返回,那就一定发生调用错误了。
例如:我随便写一个命令,这个命令是不在这个目录里的
#include <stdio.h>
#include <unistd.h>
int main()
{
// .c --> .exe --> load into memory --> process --> running
printf("The process is running...\n");
// 传参以NULL结尾,来表示传参结束
execl("/usr/bin/lsss","ls","-l","--color=auto",NULL);
printf("The process finishes running...\n");
return 0;
}
#include <stdio.h> #include <unistd.h> #include <stdlib.h> #include <assert.h> #include <sys/types.h> #include <sys/wait.h> int main() { printf("The process is running...\n"); pid_t id = fork(); assert(id != -1); if(id==0) { //child process sleep(1); execl("/usr/bin/ls","ls","-l",NULL); exit(1);// 如果调用失败,直接让子进程退出 } int status = 0; pid_t ret = waitpid(id,&status,0); if(ret == id) { printf("wait success, exit code:%d , signal number:%d\n",(status>>8)&0xFF,status&0x7F); } return 0; }
当父进程派生的子进程发生程序替换时,防止父子进程原先共享的代码段和数据段被修改,操作系统会进行写时拷贝,将代码段和数据段重新复制一份给子进程,让子进程程序替换之后,不会影响父进程。这就是进程之间的独立性。
虚拟地址空间和页表可以保证进程之间的独立性,一旦有执行流要改变代码或数据,就会发生写时拷贝。所以不是只有数据可能发生写入,代码也是有可能发生写入的,这两种情况都会发生写时拷贝。
int execl(const char *path, const char *arg, …);
int main()
{
pid_t id = fork();
if(id == 0){
// child
execl("/usr/bin/ls", "ls", "-l", NULL);
// 如果执行到这里说明替换失败,让子进程退出
exit(-1);
}
// parent
return 0;
}
int execlp(const char *file, const char *arg, …);
int main()
{
pid_t id = fork();
if(id == 0){
// child
execlp("ls", "ls", "-l", "--color=auto", NULL);
// 如果执行到这里说明替换失败,让子进程退出
exit(-1);
}
// parent
return 0;
}
int execv(const char *path, char *const argv[]);
int main()
{
pid_t id = fork();
if(id == 0){
// child
char* const argv[] = {(char*)"ls", (char*)"-l", (char*)"--color=auto", NULL};
execv("/usr/bin/ls", argv);
// 如果执行到这里说明替换失败,让子进程退出
exit(-1);
}
// parent
return 0;
}
PATH和vector,指的是不用传程序路径,默认在环境变量中进行查找并且可以将执行参数放到数组里面,统一进行传参
int execvp(const char *file, char *const argv[]);
int main()
{
pid_t id = fork();
if(id == 0){
// child
char* const argv[] = {(char*)"ls", (char*)"-l", (char*)"--color=auto", NULL};
execvp("ls", argv);
// 如果执行到这里说明替换失败,让子进程退出
exit(-1);
}
// parent
return 0;
}
int execle(const char *path, const char *arg,…, char * const envp[]);
int main()
{
pid_t id = fork();
if(id == 0){
// child
char* const env[] = {(char*)"HELLO=123456789",NULL};
execle("./mybin","mybin", NULL, env);
// 如果执行到这里说明替换失败,让子进程退出
exit(-1);
}
// parent
return 0;
}
int main()
{
pid_t id = fork();
if(id == 0){
// child
extern char** environ;
execle("./mybin","mybin", NULL, environ);
// 如果执行到这里说明替换失败,让子进程退出
exit(-1);
}
// parent
return 0;
}
int execvpe(const char *file, char *const argv[],char *const envp[]);
execvpe其实就是vector+PATH+env,我们需要自己传环境变量,并且不用可变参数列表的方式传执行参数,而是用指针数组的方式来一并将执行参数传递,传程序名时可以不带程序路径,系统会帮我们找。
带e的函数都需要自己组装环境变量,可以选择自己的、或系统的、或系统和自己的环境变量。
下图exec函数族 一个完整的例子:
.PHONY:all
all:mybin myprocess
mybin:myexec.c
gcc -o $@ $^ -std=c99
myprocess:process.c
gcc -o $@ $^ -std=c99
.PHONY:clean
clean:
rm -rf myprocess mybin
int main() { char *const argv[] = {"ps", "-ef", NULL}; char *const envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL}; execl("/bin/ps", "ps", "-ef", NULL); // 带p的,可以使用环境变量PATH,无需写全路径 execlp("ps", "ps", "-ef", NULL); // 带e的,需要自己组装环境变量 execle("ps", "ps", "-ef", NULL, envp); execv("/bin/ps", argv); // 带p的,可以使用环境变量PATH,无需写全路径 execvp("ps", argv); // 带e的,需要自己组装环境变量 execve("/bin/ps", argv, envp); exit(0); }
#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; gArgv[0] = strtok(command, SEP); int index = 1; while((gArgv[index++] = strtok(NULL, SEP))); } 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; }
ls /proc/进程id
可以看到进程有两个路径,一个是cwd一个是exe,exe路径代表当前进程执行的是磁盘上的哪个路径下的程序,可以看到执行的是myproc二进制可执行程序,cwd代表current work directory,代表当前进程的工作目录,所以实际上当前路径就是当前进程的工作目录。
在模拟shell的实现代码中,cd到其他目录,pwd之后的路径实际上是没有变化的,因为pwd实际上pwd的是父进程shell的路径,而父进程的cwd路径始终是未改变的,而执行cd命令的是子进程,所以子进程的cwd路径是会改变的。
系统给我们提供了一个系统调用接口叫做chdir
,用于改变当前进程的工作目录cwd路径,实际上cd能够进入指定路径下的目录,底层实现上就是改变了shell(bash)进程的cwd路径,所以pwd时,随时随地打印出来的就是shell进程的工作目录。
所以如果我们模拟实现的shell也想实现cd改变路径的功能,实际上是不可以创建子进程的,因为子进程程序替换执行cd,父进程的工作目录是没有改变的,所以直接将这一种情况单独拿出来进行判断,在这种情况下,直接让父进程执行cd命令,修改父进程的工作目录即可。
像上面的cd命令实际上就是shell的内建命令,因为这样的命令不需要派生子进程来进行程序替换执行,直接让父进程执行就ok,这样的指令就是shell自带的命令,我们称之为内建命令或内置命令。
这也就能解释为什么echo能够打印本地变量了,我们之前将echo理解为一个可执行程序,也就是shell的子进程,但是我们说子进程只能继承父进程的环境变量,而不能继承本地变量,所以当时就陷入echo为什么能够打印出本地变量的疑问当中,因为如果echo是子进程的话,他是没有继承本地变量的。
但现在我们就知道原因了,echo实际上不是shell的子进程,而是shell的内建命令,是shell自己来执行的指令,shell当然拥有本地变量了,当然也就能够打印本地变量。
好了,本文就到这里,感谢大家的收看声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:【wpsshop博客】
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。