一、进程标识
1.pid
每个进程都有非负整数表示的唯一进程ID,即pid,其类型为pid_t类型。可用ps命令查看当前所有进程的信息,该命令可以加选项,一般使用ps -ef或ps axf(打印进程树),查看当前系统所有进程的信息。需要注意的是和fd(文件描述符)不同,pid是顺次使用的,即当10001,10002,10003被用过后,10002释放了,下个进程的pid会继续使用10004而不是回头使用最小的可用pid10002。
pid为1的进程是init进程,它是所有进程的祖先进程。
2.getpid(),getppid()
getpid()用于获取当前进程的进程号,而getppid()用于获取当前进程的父进程的进程号。
返回值为pid_t类型的进程号。
二、进程的产生
1.fork()
fork()用于产生一个子进程,这个子进程是从父进程拷贝过来的,因此除了以下这几点其他都是一摸一样的,连产生那一刻执行到的位置都是一样的,即子进程会从创建它的那一行代码开始继续往下执行。
不同点:
1)返回值不一样
2)pid和ppid不一样
3)未决信号和文件锁不一样
4)资源利用量清零
如果成功,父进程中的返回值是子进程的pid号,子进程中的返回值是0.如果失败,父进程中的返回值是-1.
另外,父子进程是写时拷贝的,当他们都是只读的时候他们会共享物理内存页,当谁要修改内容时谁就复制一份自己的新的物理内存页,去修改自己的内容。子进程中的地址是虚拟地址,指针变量的值和父进程一样,但指向的实际物理内存是独立的。
fork使用示例:
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int main(int argc,char *argv[])
{
printf("[%d]:Begin\n",getpid());
pid_t pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
printf("[%d]:Child is working\n",getpid());
}
else
{
printf("[%d]:Parent is working\n",getpid());
}
printf("[%d]:End\n",getpid());
exit(0);
}
执行结果:
父子进程结束顺序是不确定的,由调度器的调度机制决定。
需要特别注意的是:如果fork之前没有刷新缓冲区,fork之后可能会把之前的内容重复输出,从而导致错误,因此fork之前一定要刷新缓冲区。而且有些内容比如输出到文件的内容是全缓冲的,这时用\n来刷新是无效的,必须要在fork()前用fflush()来刷新缓冲区。
将输出重定向到文件中:
可以看到Begin被输出了两次。
使用fflush():
正确输出。
2.父子进程的关系
父子进程结束时机不同会产生不同影响。
1)子进程先结束
如果子进程先于父进程结束,那么子进程会成为一个僵尸进程(zombie process)。僵尸进程是指子进程已经终止,但其父进程尚未通过wait()或waitpid()等系统调用来获取子进程的终止状态。
当子进程结束时,它的终止状态(退出码)会被保存在操作系统的进程表中,同时进程表中的相关资源也会被释放。但是,子进程的进程表条目仍然存在,以便父进程可以通过wait()或waitpid()等系统调用来获取子进程的终止状态。
在子进程成为僵尸进程后,它的状态会被标记为"Z"(Zombie)或"defunct"。僵尸进程不会再执行任何代码,也不会占用系统资源,但它的进程表条目仍然存在。
父进程可以通过调用wait()或waitpid()等系统调用来等待子进程的终止,并获取其终止状态。当父进程获取到子进程的终止状态后,操作系统会将僵尸进程的进程表条目从进程表中删除,释放相关资源。
需要注意的是,如果父进程没有及时调用wait()或waitpid()等系统调用来处理僵尸进程,那么系统中可能会存在大量的僵尸进程,这可能会导致系统资源的浪费。因此,父进程应该及时处理僵尸进程,以避免这种情况的发生。
2)父进程先结束
当父进程先于子进程结束时,子进程会成为一个孤儿进程(orphan process)。孤儿进程是指其父进程已经终止的进程。在这种情况下,子进程的新的父进程会被设置为init进程。
孤儿进程的状态取决于其具体的执行情况。如果孤儿进程仍在执行,则它将继续运行直到完成或被终止。如果孤儿进程在执行期间需要等待某些事件(如I/O操作),则它会被挂起,直到事件发生或被终止。
一旦孤儿进程终止,它的资源将被操作系统回收。这包括释放内存、关闭打开的文件描述符等。孤儿进程的终止状态(退出码)将被保存,以便父进程可以通过wait()或waitpid()等系统调用来获取。
需要注意的是,孤儿进程的终止状态不会被发送给其父进程。父进程无法通过常规的方式获取孤儿进程的终止状态。如果父进程希望获取孤儿进程的终止状态,可以使用wait()或waitpid()等系统调用来等待孤儿进程的终止,并获取其终止状态。
三、进程的回收
1.wait(),waitpid()
wait()和waitpid()都是用来回收子进程的资源的,他们会等待子进程状态变化并获取其状态信息。
wait()会阻塞等待直到任意一个子进程状态改变,而waitpid可以指定某个子进程,并且可以设置为非阻塞。waitpid()的第一个参数pid和第三个参数options有如下取值,其中options可以用按位或( | )组合使用。
可以看出,waitpid(-1,&status,0)和wait(&status)是等效的。
status参数是用来保存子进程状态的,有如下的宏定义。
二者的返回值差不多,成功返回pid,失败返回-1,不同的是如果waitpid()的options参数设置了WNOHANG(不阻塞),并且调用时没有待回收的子进程,则会返回0.
四、exec函数簇
我们知道,在shell下执行的可执行文件其父进程时shell,但fork()只能产生和自身一模一样的进程,而我们的可执行文件并不和shell一样,这是怎么实现的呢?
其实shell中执行文件产生的进程是通过exec函数簇实现的。exec函数簇可以用新的进程映像(process image)代替旧的进程映像,并在新的进程中从头执行。
换句话说,exec函数簇会将当前进程替换为新的程序,新程序会从头开始执行。它会重新加载新程序的代码段、数据段和堆栈,并开始执行新程序的入口函数。
特别注意的是,和fork()一样,也要注意缓冲区的刷新问题,所以调用exec函数簇的函数前要用fflush()刷新缓冲区。
1.execl(),execlp(),execle()
1)execl()接受一个文件路径 path
和一系列的参数,以 NULL 结尾。参数 arg0
是新程序的名称,它会作为 argv[0]
传递给新程序,后面的参数是传递给新程序的命令行参数,最后一个参数必须是 NULL。例如:
execl("/bin/ls", "ls", "-l", NULL);
2)execlp()与execl()类似,但是它会在系统的 PATH 环境变量中搜索指定的可执行文件,并执行找到的第一个匹配的文件。例如:
execlp("ls", "ls", "-l", NULL);
3)execle()与execl()类似,但是它还接受一个环境变量数组 envp
,用于设置新程序的环境变量。此 envp
是新进程的所有环境变量,其他未设置的环境变量不会继承。环境变量数组的最后一个元素必须是 NULL。例如:
char *env[] = {"PATH=/usr/bin", "HOME=/home/user", NULL};
execle("/bin/ls", "ls", "-l", NULL, env);
这些函数如果成功都没有返回值,否则返回-1.
2.execv(),execvp(),execvpe()
1)execv()接受一个文件路径 path
和一个参数数组 argv
,其中 argv[0]
是新程序的名称,后面的元素是传递给新程序的命令行参数,最后一个元素必须是 NULL。例如:
char *args[] = {"ls", "-l", NULL};
execv("/bin/ls", args);
2)execvp()与execv()类似,但是它会在系统的 PATH 环境变量中搜索指定的可执行文件,并执行找到的第一个匹配的文件。例如:
char *args[] = {"ls", "-l", NULL};
execvp("ls", args);
3)execvpe()与execvp()类似,但是它还接受一个环境变量数组 envp
,用于设置新程序的环境变量。此 envp
是新进程的所有环境变量,其他未设置的环境变量不会继承。环境变量数组的最后一个元素必须是 NULL。例如:
char *args[] = {"ls", "-l", NULL};
char *env[] = {"PATH=/usr/bin", "HOME=/home/user", NULL};
execvpe("ls", args, env);
这些函数如果成功都没有返回值,否则返回-1.
以下是一个使用示例:
int main()
{
puts("Begin");
fflush(NULL);
pid_t pid = fork();
if(pid < 0)
{
perror("fork()");
exit(1);
}
if(pid == 0)
{
execl("bin/date","date","+%s",NULL);
perror("execl()"); //走到这说明execl失败了
exit(1);
}
wait(NULL);
puts("End");
exit(0);
}
运行结果:
五、mysh的实现
了解了上面的函数,我们就可以自己实现一个简单的shell了,shell可以执行内部命令和外部命令,内部命令我们目前知识还不够,所以当前只需实现可以执行外部命令的shell.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define MAX_ARGC 20
char **parse(char *line)
{
char **args = (char**)malloc(MAX_ARGC * sizeof(char*));
char *saveptr;
char *arg;
args[0] = strtok(line," ");
int i = 1;
while((arg = strtok(NULL," ")) != NULL)
{
args[i] = arg;
i++;
}
args[i] = NULL;
return args;
}
int main(int argc,char **argv)
{
while(1)
{
printf("[user@myshell]");
char *line = NULL;
size_t n = 0;
getline(&line,&n,stdin);
line[strlen(line)-1] = '\0'; //把\n去掉,否则会影响命令行参数
char **args = parse(line);
pid_t pid = fork();
if(pid < 0)
{
perror("fork():");
}
if(pid == 0)
{
execvp(args[0],args);
perror("exec:");
exit(1);
}
else
{
free(line);
free(args);
wait(NULL);
}
}
return 0;
}
可以执行ls,du,vim等外部命令。