代码实现进程的创建、等待、终止
进程创建
进程创建使用的是fork()函数,它的作用是从一个已存在的进程中通过复制原进程的pcb,创建一个新的子进程,原进程则为新进程的父进程。父子进程代码共享,但是数据独有。fork()函数返回值父子进程各有不同,父进程返回的是子进程的pid,而子进程返回的是0,我们可以通过返回值的不同实现父子进程的代码分流。
代码实现:
/*这是一个演示进程创建,实现父子进程代码分流,数据独有的demo
*/
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
int g_val=100;
int main()
{
int pid =fork();
if(pid<0)
{
perror("fork error");
exit(-1);
}
if(0==pid)
{
g_val=20;
printf("I am child!!! pid:%d g_val:%d\n",getpid(),g_val);
exit(1);
}
else if(pid>0)
{
sleep(1);
printf("I am parent!!! pid:%d g_val:%d\n",getpid(),g_val);
}
return 0;
}
运行结果:
从以上代码和运行结果可以看出,我们利用了fork()函数返回值的不同实现了父子进程的代码分流,让父子进程分别打印不同的语句。另外,我们定义的int g_val=100,虽然在子进程中修改其为20,但并没有改变父进程中的值,由此可见,虽然父子进程代码是共享的,但数据独有。
进程终止
进程退出的场景:
- 结果正确,正常退出
- 正常退出,结果错误
- 异常退出
进程的退出方式:
- 调用exit库函数
- 调用_exit系统接口
- main函数中return
下面用代码来看看三种方式的实现和区别:
调用exit库函数:
代码实现:
/*这是一个演示进程退出的demo
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello linux!");
exit(1);
}
运行结果:
exit库函数其实是_exit系统接口的封装,调用exit函数最后也会调用_exit,只不过在调用_exit之前,还会刷新缓冲区,关闭所有的流。
调用_exit系统接口
代码实现:
/*这是一个演示进程退出的demo
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello linux!");
_exit(1);
}
运行结果:
上面可以看到,调用_exit系统接口,打印语句没有输出内容。这是因为_exit的退出方式是粗暴的,直接释放所有占用资源退出,因此缓冲区内的数据没有输出就直接被释放了。
main函数中return
代码实现:
/*这是一个演示进程退出的demo
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
printf("hello linux!");
return 0;
}
运行结果:
执行return n就相当于执行exit(0),最后会把main函数的返回值当做exit的参数。
进程等待
子进程先于父进程退出会成为僵尸进程;为了避免产生僵尸进程,父进程就需要关注进程的退出状态(操作系统的通知),但是子进程什么时候退出或者说这个退出通知什么时候发给父进程,父进程根本不确定,因此只能一直等待这个退出通知。
如何实现进程等待呢?一般使用的是两个函数:
- pid_t wait(int *status);
等待子进程退出获取退出返回值,允许操作系统释放子进程资源
status:输出型参数,用于获取子进程的退出返回值
返回值:退出子进程的pid
wait是一个阻塞型函数,没有子进程退出则一直等待 - pid_t waitpid(pid_t pid, int *status, int options);
pid: 等待指定子进程; -1:等待任意子进程
status:输出型参数,用于获取子进程的退出返回值
options: WNOHANG—设置waitpid为非阻塞函数
waitpid默认等待子进程退出,WNOHANG:没有子进程退出则立即报错返回
返回值:退出子进程的pid ;0-当前没有子进程退出 ;<0-出错
注意:
两个函数中都有一个status参数,它是用于获取子进程的退出返回值,如果传递NULL则表示不关心子进程的退出状态信息。
子进程返回的数据只有一个字节,但waitpid函数获取到的是4个字节,剩下字节中存储的是其他数据,因此直接打印这个statu将无法获取到返回值。低7位中存储进程异常退出的信号值,并且这低7位用于判断一个进程是否是正常退出。(0正常退出)
代码实现(waitpid):
/*这是一个演示进程等待的demo
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/wait.h>
int main()
{
pid_t pid=fork();
if(pid<0)
{
perror("fork error");
exit(-1);
}
else if(0==pid)
{
printf("child is run,pid is:%d\n",getpid());
sleep(5);
exit(233);
}
else
{
int status=0;
int ret=waitpid(-1,&status,0);
printf("father is waiting for child exit\n");
if(WIFEXITED(status)&&ret==pid)
{
printf("wait child success,child exit code is:%d\n",WEXITSTATUS(status));
}
else
{
printf("wait child failed...\n");
return 1;
}
}
return 0;
}
运行结果:
编写minishell
首先,要知道shell可以说就是一个命令行解释器,它的运行是一个怎样的过程呢:
从标准输入读取数据(scanf)
例如:ls----运行ls命令-----ls命令是一个外部命令
创建一个进程,让这个进程运行ls命令
读取输入->命令解析->创建子进程->(子进程)程序替换->(父进程)进程等待
代码实现:
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
int main()
{
while(1)
{
char cmd[1024]={0};
printf("minshell: ");
fflush(stdout);
//scanf读取缓冲区数据的时候,默认遇到空格终止
//%[^\n]:将\n之前的数据放入到cmd中,设置遇到\n终止读取
//%*c:将剩下的字符全部从缓冲区取出来,丢弃
if(scanf("%[^\n]%*c",cmd)!=1)
{
getchar();
}
int pid=fork();
if(pid<0)
{
exit(-1);
}
else if(0==pid)
{
char *ptr=cmd;
int redirect_flag=0;
char *file;
while(*ptr!='\0')
{
if(*ptr=='>')
{
*ptr++='\0';
redirect_flag=1;
if(*ptr=='>')
{
*ptr++='\0';
redirect_flag=2;
}
while(*ptr!='\0'&&isspace(*ptr))
{
ptr++;
}
file=ptr;
while(*ptr!='\0'&&!isspace(*ptr))
{
ptr++;
}
*ptr='\0';
}
ptr++;
}
int fd=-1;
if(redirect_flag==1)
{
fd=open(file,O_CREAT|O_TRUNC|O_WRONLY,0664);
dup2(fd,1);
}
else if(redirect_flag==2)
{
fd=open(file,O_CREAT|O_APPEND|O_WRONLY,0664);
dup2(fd,1);
}
ptr=cmd;
char *argv[32]={NULL};
int argc=0;
while(*ptr!='\0')
{
//*ptr非空白字符则为参数首地址 l位置
//isspace判断字符是否是空白字符
if(!isspace(*ptr))
{
argv[argc++]=ptr;
//将参数主体走完 ls
while(!isspace(*ptr) && *ptr!='\0')
ptr++;
//第一次得到ls 第二次进来得到-l
continue;
}
else
{
*ptr='\0';
}
ptr++;
}
execvp(argv[0],argv);
exit(-1);
}
else
{
waitpid(pid,NULL,0);
}
}
return 0;
}
运行结果:
封装fork/wait等操作
代码实现:
/*封装fork/wait等操作, 编写函数 process_create(pid_t* pid, void* func, void* arg), func回调函数就是子进程执行的入口函数, arg是传递给func回调函数的参数
*/
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <errno.h>
int process_create(int (*func)(),const char* file,char *argv[])
{
pid_t pid=fork();
if(pid<0)
{
perror("fork error");
exit(-1);
}
if(0==pid)
{
int ret=func(file,argv);
if(ret<0)
{
perror("func error");
exit(0);
}
}
else
{
int st;
pid_t ret=wait(&st);
if(ret<0)
{
perror("wait error");
exit(1);
}
}
return 0;
}
int main()
{
char *argv1[]={"ls"};
char *argv2[]={"ls","-al","/etc/passwd",0};
process_create(execvp,*argv1,argv2);
return 0;
}
执行结果:
popen/system
popen()函数
创建一个管道用于进程间通信,并调用shell,因为管道被定义为单向的 所以 type 参数 只能定义成 只读或者 只写, 不能是 两者同时, 结果流也相应的 是只读 或者 只写.
函数原型:
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);
函数功能:popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。这个进程必须由 pclose 关闭。
command参数 是 一个 字符串指针, 指向的是一个 以 null 结束符结尾的字符串, 这个字符串包含一个 shell命令. 这个命令被送到 /bin/sh 以 -c 参数 执行, 即由 shell 来执行.
type 参数也是 一个 指向 以 null 结束符结尾的 字符串的指针
参数type可使用“r”代表读取,“w”代表写入。
依照此type值,popen()会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
返回值:
若成功则返回文件指针,否则返回NULL,错误原因存于errno中
system函数
system()会调用fork()产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令,此命令执行完后随即返回原调用的进程。在调用system()期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。
int system(const char *command);
实际上system()函数执行了三步操作:
1.fork一个子进程;
2.在子进程中调用exec函数去执行command;
3.在父进程中调用wait去等待子进程结束。
返回值:
1>如果 exec 执行成功,即 command 顺利执行,则返回 command 通过 exit 或 return 的返回值。(注意 :command 顺利执行不代表执行成功,当参数中存在文件时,不论这个文件存不存在,command 都顺利执行)
2>如果exec执行失败,也即command没有顺利执行,比如被信号中断,或者command命令根本不存在, 返回 127
3>如果 command 为 NULL, 则 system 返回非 0 值.
4>对于fork失败,system()函数返回-1。
判断一个 system 函数调用 shell 脚本是否正常结束的方法的条件应该是
1.status != -1
2.(WIFEXITED(status) 非零且 WEXITSTATUS(status) == 0