程序员成长之旅 ——进程控制
代码实现进程创建、等待、终止
进程创建
(1) fork函数创建进程
fork调用格式如下:
#include<unistd.h>
pid_t pid = fork();
返回值:子进程返回0,父进程返回子进程的pid,创建失败返回-1
代码如下:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
pid_t pid;
printf("before: pid is %d\n",getpid());
if((pid = fork()) == -1)//fork()子进程返回0,父进程返回子进程pid
{
perror("fork");
exit(1);
}
printf("after:pid is %d,fork return is %d\n",getpid(),pid);
sleep(1);
return 0;
}
运行结果如下:
从上面可看出fork之前父进程独立运行,fork之后一起运行,但是谁先运行这是不确定的。
(2) vfork创建进程
fork与vfork使用的区别:
- vfork创建的子进程与父进程共享地址空间,fork的子进程具有独立的地址空间
- vfork保证子进程先运行,在它调用exec或exit之后父进程才可能被调度
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int flag = 100;
pid_t pid = vfork();
if(pid == -1)
{
perror("vfork()");
exit(1);
}
if(pid == 0)//child
{
flag = 200;
printf("child flag is %d\n",flag);
exit(0);
}
else//parent
{
printf("parent flag is %d\n",flag);
}
return 0;
}
可以看到:子进程改变了父进程的变量值,因为子进程在父进程的地址空间中运行。
我们再来用fork验证一下
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int flag = 100;
pid_t pid = fork();
if(pid == -1)
{
perror("fork()");
exit(1);
}
if(pid == 0)//child
{
flag = 200;
printf("child flag is %d\n",flag);
exit(0);
}
else//parent
{
printf("parent flag is %d\n",flag);
}
return 0;
}
发现父进程并没有被修改,这是因为子进程有自己独立的地址空间。
进程终止
进程退出的三种场景
代码运行完毕,结果对,正常退出
代码运行完毕,结果不对,正常退出
代码异常退出
进程常见的退出方法
- 正常终止(可以用echo $?查看进程退出码)
从main函数返回;调用exit;_exit;- 异常退出
Ctrl + c 信号终止
终止进程场景演示如下
- 从main函数返回终止进程场景演示如下:
#include <stdio.h>
int main()
{
printf("hello lpf\n");
return 0;
}
运行该程序,可以查看程序退出码为0
将main函数中的0改为9,再运行可以看出程序的退出码则为9
结论:
main函数中的return语句返回的即为程序的退出码,其它函数中的return语句返回值并不是
echo $?仅能查看最近一条命令的退出码
退出码0表示程序正常退出结果正确
- 调用_exit函数终止进程的场景演示如下
_exit函数使用方法如下:
#include<stdlib.h>
void _exit(int status);
参数:status定义了进程的中止状态,父进程通过wait来获取该值
注意:虽然status类型是int,但是仅有低8位可以被父进程所用。所以_exit(-1)时,在终端查看退出是255
#include <stdio.h>
#include <unistd.h>
void test()
{
_exit(1);
}
int main()
{
printf("hello lpf\n");
test();
return 0;
}
结论:_exit函数会直接中止程序,无论该函数在主函数还是调用函数中
- 调用exit函数终止进程场景如下
#include<unistd.h>
void exit(int status);
exit函数的实现最后也会调用_exit函数,但在调用_exit函数之前,还会做其他工作( 比如刷新缓冲区 ),所以它和_exit函数不太一样,它其实不是直接中止程序的。
#include <stdio.h>
#include <stdlib.h>
void test()
{
exit(1);
}
int main()
{
printf("hello lpf");
test();
return 0;
}
运行程序,会发现同样的代码,只是将_exit换成了exit。printf中的语句就被输出来了
return是一种更常见的退出进程方法。执行return n等同于执行exit(n),因为调用main的运行时函数会将main的返
回值当做 exit的参数。
进程等待
进程等待的方式一般有两种:
阻塞式等待:子进程没有退出时,父进程一直等待子进程的退出(常见于调用函数时)
非阻塞式等待:采取轮询式访问,条件不满足时,父进程会返回去做其他事
(1)wait方法
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status)
返回值:成功返回被等待进程的pid,失败返回-1
参数status:输出型参数,获取子进程退出状态,不关心子进程的退出原因时,可设置为NULL
代码如下:
#include <stdio.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h> //exit
int main()
{
pid_t id = fork();
if(id == 0)//child
{
int count = 5;
while(count--)
{
printf("hello %d,pid is %d\n",count,getpid());
sleep(2);
}
}
else if(id > 0)//father
{
int st;
int ret = wait(&st);
if(ret>0 && (st&0X7F)==0)//正常退出(status参数的低8位未收到任何信号)
{
printf("child exit code:%d\n",(st>>8)&0XFF);//让st的次低8位与8个比特位均为1按位与,得到退出码
}
else if(ret > 0)
{
printf("sig code:%d\n",st&0X7F);//让st的低7位分别与1按位与,得到终止信号
}
}
else
{
perror("fork");
exit(1);
}
return 0;
}
当程序自己执行完后,程序正常退出,收到0号退出码。
在另一个在另一个终端kill该进程,程序异常退出,收到15号SIGTERM程序结束信号。与SIGKILL不同的是该信号可以被阻塞和处理,通常用来要求程序正常退出。
下面测试的是,利用9号信号杀死该进程,程序异常退出,收到9号信号。
(2)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。
#include <stdio.h>
#include <unistd.h>//fork
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)//child
{
printf("child is run,pid is %d\n",getpid());
sleep(5);
exit(3);
}
else//parent
{
int status = 0;
pid_t ret = waitpid(-1,&status,0);//阻塞式等待
printf("I am waiting\n");
if(WIFEXITED(status) && ret == id)//WIFEXITED(status)为真且正常返回才进入该条件语句
{
printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看进程的退出码
}
else
{
printf("wait child failed,return \n");
return 2;
}
}
return 0;
}
让程序运行,自己退出。因为子进程用exit(3)退出,所以SEXITSTATUS(status)非零,提取出子进程的退出码3。
当我在另一个终端下,kill掉该进程,子进程异常退出,所以WIFEXITED(status)不为真,所以父进程中走else语句,输出结果如下:
以下是waitpid函数的非阻塞式等待实现:
#include <stdio.h>
#include <unistd.h>//fork
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
int main()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return 1;
}
else if(id == 0)//child
{
printf("child is run,pid is %d\n",getpid());
sleep(10);
exit(3);
}
else//parent
{
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1,&status,WNOHANG);//非阻塞式等待
if(ret == 0)//无退出子进程可收集
{
printf("child is running\n");
}
sleep(1);
}while(ret == 0);//无退出子进程时,会一直询问(轮询式访问),这里1s询问一次
if(WIFEXITED(status) && ret == id)//WIFEXITED(status)为真且正常返回才进入该条件语句
{
printf("wait success,child return code is %d\n",WEXITSTATUS(status));//WEXITSTATUS(status))查看进程的退出码
}
else
{
printf("wait child failed,return \n");
return 2;
}
}
return 0;
}
子进程正常退出结果
换终端将子进程异常终止,运行结果如下
(3)获取子进程status
- wait和waitpid,均有一个status参数,该参数是一个输出型参数,由操作系统填充;
- 若传NULL,表示不关心子进程的退出状态信息;
- 否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程
- status不能当做简单整型看待,可以当做位图来看待,具体如下图(这里只有低16位比特位):
由图可知,当程序正常终止时,status的位图的低8位不会收到任何信号,保持全0状态,此时位图的次低8位则保存着程序的退出码。一旦低8位收到信号,则表示程序被信号杀死即异常终止。此时位图的低8位保存程序的终止信号,次低8位不会用。不过需要注意的是,实际上用来保存收到终止信号的只有7个比特位,因为低8位中有一个比特位是core dump标志。
coredump标志 : 核心转储标志
在程序异常退出时,保存程序的堆栈信息,方便事后调试 (核心转储通常默认是关闭的: 安全隐患 和 空间占用)
迷你自主shell的编写
- 获取命令行
- 解析命令行
- 建立一个子进程
- 替换子进程
- 父进程等待子进程退出
#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<errno.h>
#include<sys/wait.h>
int main()
{
while(1){
printf("[lpf@localhost]# ");
fflush(stdout);
char buf[1024] = {0};
fgets(buf,1023,stdin);//从标准输入获取用户敲击的命令
buf[strlen(buf) - 1] = '\0';
printf("cmd:[%s]\n",buf);
//解析
int argc = 0;
char *argv[32] = {NULL};
char *ptr = buf;
while(*ptr != '\0'){
if(*ptr != ' ')
{
argv[argc++] = ptr;
while(*ptr != ' ' && *ptr != '\0'){
ptr++;
}
*ptr = '\0';
}
ptr++;
}
argv[argc] = NULL;
int i;
for(i = 0;i < argc;i++)
{
printf("[%s]\n",argv[i]);
}
int pid = fork();
if(pid == 0)
{
execvp(argv[0],argv);
exit(0);
}
waitpid(-1,NULL,0);
}
return 0;
}
封装fork/wait等操作,编写函数process_create ( pid_t* pid, void* func, void* arg ),func回调函数就是子进程执行的入口函数,arg是传递给func回调函数的参数
代码:
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<string.h>
void func(char *arg[])//子进程要执行的函数
{
execvp(arg[1],arg+1);//因为在输入的时候,argv里的第一个参数是可执行程序的名字,不是要执行的参数,所以要从argv里的第二个参数
//比如要输入:./process_create ls -l
//argv[0] --->./process
//argc[1] --->ls
//argc[2] --->-l
exit(1);
}
typedef void(*FUNC)(char**);
void process_creak(pid_t* pid,void* func,char* arg[])
{
*pid = fork();//创建一个进程
if(*pid == 0)
{
//子进程
FUNC funct = (FUNC)(func);
(*funct)(arg);
}else
if(pid > 0)
{
//父进程
wait(NULL);
}else
{
//创建进程失败
perror("fork");
exit(1);
}
}
int main(int argc,char* argv[])
{
// ./process_creak 要执行的参数列表
// 比如: ./process_creak ls -l
if(argc == 1)
{
fprintf(stderr,"usage:%s 参数\n",argv[0]);
exit(1);
}
pid_t pid;
process_creak(&pid,func,argv);
}
popen/system这两个函数和fork的区别.
1.popen函数
(1)函数原型
#include<stdio.h>
FILE *popen(const char *command,const char *type);
int pclose(FILE *stream);
创建一个管道用于进程间通信,并调用shell,因为管道被定义为单向的。所以 type 参数只能定义成只读或者只写, 结果流也相应的是只读或只写。
(2)函数功能
popen()会调用fork()产生子进程,然后从子进程中调用/bin/sh -c来执行参数command的指令。这个进程必须由pclose关闭。
(3)参数
command :一个字符串指针, 指向一个以NULL为结束符的字符串。 这个字符串包含一个shell命令, 这个命令被送到 /bin/sh 以-c参数 执行, 即由 shell 来执行。
type :一个指向以NULL为结束符的字符串指针,用“r”代表读,“w“代表写。依照type值, popen( )会建立管道连到子进程的标准输出设备或标准输入设备,然后返回一个文件指针。随后进程便可利用此文件指针来读取子进程的输出设备或是写入到子进程的标准输入设备中。
(4)返回值
成功则返回文件指针,否则返回NULL,错误原因存于errno中。
编写代码如下:(将replace.c文件的内容输出到屏幕)
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main()
{
FILE* file = NULL;
char buf[1024] = {0};
file = popen("cat replace.c","r");
if(file == NULL)
{
perror("popen");
exit(1);
}
while(fgets(buf,1024,file) != NULL)
{
fprintf(stdout,"%s",buf);
}
pclose(file);
return 0;
}
2.system函数
(1)函数原型
#include<stdlib.h>
int system(const char *command)
(2)函数功能
system( )会调用fork( )产生子进程,由子进程来调用/bin/sh-c string来执行参数string字符串所代表的命令。此命令执行完后随即返回原调用的进程。在调用system( )期间SIGCHLD 信号会被暂时搁置,SIGINT和SIGQUIT 信号则会被忽略。调用/bin/sh来执行参数指定的命令,/bin/sh 一般是一个软连接,指向某个具体的shell。
实际上system()函数执行了三步操作:
fork一个子进程;
在子进程中调用exec函数去执行command;
在父进程中调用wait去等待子进程结束。
(3)返回值
若 exec 执行成功,即 command 顺利执行,则返回 command 通过 exit 或 return 的返回值。(注意 :command 顺利执行不代表执行成功,当参数中存在文件时,不论这个文件存不存在,command 都顺利执行) ;
若exec执行失败,即command没有顺利执行,比如被信号中断,或者command命令根本不存在, 返回 127 ;
若command 为 NULL, 则 system 返回非 0 值;
对于fork失败,system()函数返回-1。注意:判断一个system函数调用shell是否正常结束的标志是status != -1;(WIFEXITED(status))非零且WEXITSTATUS(status) == 0
代码
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
int main()
{
int status = 0;
status = system("ls -l");
if(status == -1)
{
perror("system");
exit(1);
}
if(WIFEXITED(status) != 0)//正常退出
{
if(WEXITSTATUS(status) == 0)//操作正确
{
printf("run success\n");
}
else
{
printf("run failed,exit code is %d\n",WEXITSTATUS(status));
}
}
else//异常退出
{
printf("sig code is %d\n",WEXITSTATUS(status));
}
}
3.区别
(1)system 在执行期间,调用进程会一直等待 shell 命令执行完成(waitpid),但是 popen 无需等待 shell 命令执行完成就返回。
(2)popen 函数执行完毕后必须调用 pclose 来对所创建的子进程进行回收,否则会造成僵尸进程的情况。
(3)open 没有屏蔽 SIGCHLD ,如果在调用时屏蔽了 SIGCHLD ,且在 popen 和 pclose 之间调用进程又创建了其他子进程并调用进程注册了 SIGCHLD 来处理子进程的回收工作,那么这个回收工作会一直阻塞到 pclose 调用。