进程创建/等待/终止
进程创建
#include <unistd.h>
pid_t fork(void);
pid_t vfork(void);
pid_t fork(void); 通过复制父进程来创建子进程,子进程因为拷贝了父进程pcb中的很多数据(在内核中调用clone完成),因此与父进程内存指针以及程序计数器相同,所以运行的代码以及运行位置一样(采用写时拷贝技术,提高效率)。
父子进程之间:代码共享,数据独有。
返回值: -1 创建子进程失败;0 对于子进程,返回值为0;>0 对于父进程,返回值是子进程的pid。
pid_t vfork(void);0 创建一个子进程(内核中调用clone完成),但是创建子进程之后父进程会阻塞(挂起),直到子进程退出或者程序替换。创建的子进程与父进程共用同一个虚拟空间(故不需要为子进程创建虚拟地址空间/页表),为了防止调用栈混乱,因此阻塞父进程。
进程等待
父进程等待子进程的退出,获取这个子进程的返回值,并且回收子进程的所有资源,避免产生僵尸进程。
#include <sys/wait.h>
int wait(int *status); // 一个系统调用接口
pid_t waitpid(pid_t pid, int *status, int options);
int wait(int *status); 该接口等待任意一个子进程退出,是一个阻塞接口,若没有子进程则报错,若没有子进程退出则一直等待,子进程的返回值会放到传入的参数status中。
pid_t waitpid(pid_t pid, int *status, int options);
1.可以等待指定pid的子进程退出
2.options若被指定为WNOHANG(默认为阻塞),则将waitpid设置为非阻塞,则通常需要循环处理
3.pid设置值:-1 表示等待任意一个子进程;>0 表示等待指定pid的子进程
返回值:>0 返回的是退出的子进程的pid;=0 没有子进程退出;-1 等待出错。
进程终止
终止场景:异常退出;正常退出,结果符合预期;正常退出,结果不符合预期
#include <stdio.h>
void perror(const char *s); // 打印上次系统调用的错误信息
#include <errno.h>
char *sys_err_list[] / int errno
#include <string.h>
char *strerror(int errnum); // 获取上一次系统调用的错误
// 获取一个系统调用的错误信息
void perror(char* msg);
char *strerrno(int errno);
如何终止一个进程:
- 在main函数中return(return时刷新缓冲区)
- 在任意地方用exit(int statu)接口(库函数接口,头文件stdlib.h)退出前刷新缓冲区
- 在任意地方用_exit(int statu)接口(系统调用接口)立即退出,释放进程所有资源,对于系统调用来说,不存在上述两种方式所说的缓冲区
模仿编写shell
#include <sys/types.h>
#include <sys/wait.h>
int execvp(const char* file, const char* argv[]);
const char* file是要运行的文件,会在环境变量PATH中查找file执行;
const char* argv[]是一个参数列表,如同在shell中调用程序一样,参数列表为0,1,2,3……因此第0个参数重复一遍;
const char* argv[]最后一个必须是 NULL;
失败返回-1且在当前进程运行, 成功无返回值,直接结束当前进程。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <errno.h>
#include <sys/wait.h>
#include <ctype.h>
#define N 1024
#define MIX 32
int main()
{
while (1)
{
// 输入提示
printf("[mini@localhost]# ");
// 清空缓冲区
fflush(stdout);
char buf[N] = {0};
// 获取输入的字符串,一直接收至遇到换行符
fgets(buf, N - 1, stdin);
buf[strlen(buf) - 1] = '\0';
printf("cmd: [%s]\n", buf); // 查看数据接收是否正确
// 解析输入字符串,获取需要运行的程序名称及参数
int argc = 0;
char *argv[MIX] = {NULL}; // 指针数组
char *ptr = buf;
while (*ptr != '\0')
{
if (!isspace(*ptr))
{
argv[argc] = ptr;
argc++;
while (!isspace(*ptr) && *ptr != '\0')
ptr++;
*ptr = '\0';
}
ptr++;
}
argv[argc] = NULL;
int i = 0;
for (i = 0; i < argc; i++)
printf("[%s]\n", argv[i]); // 查看数据解析是否分段正确
// 这里输出加\n为了刷新缓冲区内容,如果用空格则会在程序本次循环退出时才输出这段内容
// 程序替换,调用子进程
int pid = fork();
if (pid == 0) // 子进程
{
if (argv[0] == "cd") // 单独处理cd指令
chdir(argv[1]); // chdir 是C语言中的一个系统调用函数(同cd)
execvp(argv[0], argv);
exit(0);
}
waitpid(-1, NULL, 0); // 父进程等待子进程结束
}
return 0;
}
// 经测试运行程序在输入指令的时候,backspace键按下会显示^H,无法删除已输入的字符
// 按下delete键会显示^[[3~,同样无法删除已输入的字符
以上代码在运行时如输入pwd,则会显示当前代码所在文件路径,但是运行时输入cd命令依旧无法跳转文件目录。
封装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>
void process_create(pid_t* pid, void* func, void* arg)
{
*pid = fork();
pid_t id = 0;
if (*pid < 0)
{
perror("fork error");
return;
}
else if (*pid == 0) // child 子进程运行传入的函数及参数
{
// 返回值为int的函数指针function
// execvp的返回值为int
// 两个传入的参数先强转为char*再解引用
((int(*)())func) (((char**)arg)[0], (char**)arg);
exit(0);
}
else // father
{
while (wait(NULL) != *pid)
{
id = wait(NULL);
printf("child id: %d\n", id);
return;
}
}
return;
}
int main()
{
pid_t pid = 0;
char *arg[] = {"ls", "-l", NULL};
process_create(&pid, execvp, arg);
return 0;
}
popen/system与fork
#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream); // 调用失败则返回-1
popen()函数是标准c提供的一个管道创建函数,其内部操作主要是创建一个管道,调用fork创建子进程,关闭不需用的文件描述符,调用exec函数族执行popen的第一个参数。然后等到关闭。管道为半双工工作,所以type参数只能定义成只读(r)、只写(w), 不能可读可写,结果流也只读 、只写。
popen()调用fork()产生子进程,子进程中调用/bin/sh -c来执行参数command的指令。这个进程必须由 pclose 关闭。
command参数是字符串指针,指向以NULL结束符结尾的字符串,这个字符串包含一个shell命令,命令被送到 /bin/sh 以 -c 参数执行,即由 shell 来执行。
如果type是“r”则文件指针连接到command的标准输出;如果type是“w”则文件指针连接到command的标准输入。
返回值: 成功则返回文件指针,失败返回NULL,错误原因可由errno查看。
返回值是一个普通的标准I/O流,只能用 pclose() 函数关闭。
#include <stdlib.h>
int system(const char *command);
system()调用fork()产生子进程,子进程调用/bin/sh-c string(/bin/sh 一般是一个软连接,指向某个具体的shell)执行参数字符串所代表的命令,命令执行完后返回原调用的进程。即:fork一个子进程;子进程中调用exec函数执行command;父进程中调用wait等待子进程结束。 调用system()期间SIGCHLD 信号被暂时搁置,SIGINT和SIGQUIT 信号被忽略。
返回值:-1 出现错误;0 调用成功但是没有出现子进程;>0 成功退出的子进程的id。
system()调用/bin/sh时失败返回127,其他失败原因返回-1。若参数为空指针,则返回非零值。
system()调用成功则返回执行shell命令后的返回值,但是此返回值有可能为127,因此最好再检查errno从而确认执行是否成功。
system()继承环境变量,所以在编写具有SUID/SGID权限的程序时不使用system(),以防通过环境变量可能造成的系统安全问题。