Linux系统编程——Linux进程控制
1. 进程创建
我们知道,进程是描述程序的执行过程,那么我们可以在程序的内部创建其他进程,用来调用其他程序执行过程。Linux中的fork
函数,其作用就是创建一个新的进程,而这个进程被称为子进程。
#include <unistd.h>
pid_t fork(void);
分析一下该函数,它有多个返回值,如果函数正常创建了子进程,子进程中返回0,父进程返回子进程的ID,如果出错返回-1。
为什么一个函数可以根据不同的进程所返回不同的结果呢?我们的进程在调用了fork()
函数之后,其内部做了以下事情。
- 分配了一个新的内存块和内核数据结构给子进程。
- 将父进程部分数据结构内容拷贝给子进程
- 添加子进程到系统进程列表中
- fork返回,开始调度器调度
当一个进程在调用fork
的时候,其实就会赋值当前进程的二进制代码给子进程,两个进程就开始各自执行之后的代码。
int main( void )
{
pid_t pid;
printf("Before: pid is %d\n", getpid());
// 如果fork失败
if ( (pid = fork()) == -1 )
{
perror("fork()");
exit(1);
}
printf("After:pid is %d, fork return %d\n", getpid(), pid);
sleep(1);
return 0;
}
执行结果:
Before: pid is 43676
After:pid is 43676 fork return 43677
After:pid is 43677, fork return 0
我们看到,这里有三行输出,一行before,两行after。父进程先打印了before的消息,然后另外父进程和子进程都有打印after的消息。但是,为什么子进程没有执行fork函数之前的代码呢?是因为在fork之后,父子两个轮流执行之后的代码,之前执行的代码不会在执行了。而after的消息输出,是父进程和子进程都有可能先输出,因为这要根据调度器来决定。你可以尝试输出几次,会得到不一样的结果。
1.1 写时拷贝
通常,父子代码共享的时候,父进程不写入的时候,数据也会共享的,这是因为,如果我们只读数据的话,两个进程共享一份相同的数据是不会有任何影响的。但当其中一方需要修改对应的数据时,这时便会采用写时拷贝的方式在对应的物理地址上复制一份副本。
2. 进程终止
进程终止有三种情况,分别如下:
- 代码运行完毕,结果正确。
- 代码运行完毕,结果不正确。
- 代码异常终止。
正常终止:
- 从main函数返回
- 调用
exit
- 调用
_exit
异常退出:
- ctrl+c ,信号终止
main函数其实是通过return来进程退出,return n
就是执行exit(n)
,而返回0表示进程正常退出,非0表示异常退出。
下面介绍exit
和_exit
。
#include <unistd.h>
void _exit(int status);
void exit(int status);
status
是状态码的意思,它定义了进程的终止状态,父进程可以通过wait
来获取子进程的返回状态吗。而在linux下,如果执行完一个进程,想获得其状态码,可以在终端获取$?
。
其实,exit本质上是调用了_exit
。并且在调用之前,还做了其他工作。
- 执行用户通过
atexit
和on_exit
定义的清理函数。- 关闭所有打开的流,所有的缓存数据均被写入。
- 调用
_exit
。
案例:
int main()
{
printf("hello");
exit(0);
}
3. 进程等待
为什么需要进程等待
呢?考虑一种情况,如果子进程退出,父进程不管子进程,就可能会造成僵尸进程的问题,从而造成内存泄露。
并且,子进程一旦称为僵尸进程,也没办法杀死,因为谁也没办法杀死一个已经死去的进程。
最后,父进程派给子进程一个任务,我们并不知道子进程完成这个任务的情况。运行结果是正确还是错误,有没有正常退出等待。
进程等待的方法:
·wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int* status);
- 返回值是返回被等待的进程pid,失败返回-1。
- 参数是获取子进程退出状态,如果不关心可以传入NULL。
·waitpid
pid_t waitpid(pid_t pid,int *status,int options);
- 返回值:
- 如果正常返回的时候,waitpid就返回子进程的id。
- 如果设置了选项
WNOHANG
,而调用的时候waitpid发现没有可回收的子进程,返回0。- 如果调用中出错,返回-1。并设置对应的errno码。
- 参数:
- pid:
- pid = -1,等待任意一个子进程。与wait一致。
- pid > 0, 等待指定的pid子进程。
- status:
WIFEXITED(status)
,若为正常终止子进程返回的状态,则为真。WEXITSTATUS(status)
,若WIFEXITED
非零,提取子进程退出码。- options:
WNOHANG
:若pid指定的子进程没有结束,立即返回0。若正常结束,返回子进程的pid。
获取子进程的status:
- wait和waitpid,都有一个status参数,该参数是由操作系统填充。
- 如果传递NULL,表示不关心子进程的退出状态信息。
- status本质上是一个位图,不能当成整形看待。(只关心低16位)
测试代码:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main(void)
{
pid_t pid;
// 创建子进程
if ((pid = fork()) == -1)
perror("fork"), exit(1);
if (pid == 0)
{
// 子进程睡眠20秒后退出,返回状态码10
sleep(20);
exit(10);
}
else
{
int st;
int ret = wait(&st);
// 如果正常退出,说明状态码低7位是全0
if (ret > 0 && (st & 0X7F) == 0)
{ // 正常退出
// 正常退出的status为101000000000 >> 8 = 1010 && 0xff = 1010 为 10
printf("child exit code:%d\n", (st >> 8) & 0XFF);
}
else if (ret > 0)
{ // 异常退出
// 异常退出,获取对应信号
printf("sig code : %d\n", st & 0X7F);
}
}
}
3.1 进程阻塞等待和非阻塞等待
阻塞等待:
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
printf("%s fork error\n", __FUNCTION__); // __FUNCTION__返回所在的函数
return 1;
}
else if (pid == 0)
{
// child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(257);
}
else
{
int status = 0;
pid_t ret = waitpid(-1, &status, 0); //阻塞式等待,等待5S
printf("this is test for wait\n");
if (WIFEXITED(status) && ret == pid)
{
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else
{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
非阻塞等待:
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid;
pid = fork();
if (pid < 0)
{
printf("%s fork error\n", __FUNCTION__);
return 1;
}
else if (pid == 0)
{ // child
printf("child is run, pid is : %d\n", getpid());
sleep(5);
exit(1);
}
else
{
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);
if (WIFEXITED(status) && ret == pid)
{
printf("wait child 5s success, child return code is :%d.\n", WEXITSTATUS(status));
}
else
{
printf("wait child failed, return.\n");
return 1;
}
}
return 0;
}
4. 进程程序替换
用fork
创建子进程后执行的是和父进程相同的程序(但可能执行不同的代码分支),子进程往往会调用exec
相关的函数来执行另一个程序。当进程调用一种exec
函数的时候,该进程的用户空间代码和数据完全被新进程替换,从新进程的启动开始执行。
替换函数:
#include <unistd.h>
extern char **environ;
int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg,..., char * const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],char *const envp[]);
这些函数,其实只做一件事情,就是进程程序替换
。可以根据命名来理解,path
为传入的路径。file
传入的是文件,会从环境变量去搜索,无需全路径。而envp
则是环境变量。最后arg,...
和argv[]
的区别在于是一个一个参数传入,还是传入一个数组。
测试代码:
#include <unistd.h>
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);
}
5. 简易shell程序
我们可以使用程序替换来做一个简易的shell。
比如实现ls
命令来查询当前列表,使用ps
获取进程状态等等。
**原理:**父进程获取命令,然后创建子进程,子进程采用程序替换来调用这些命令对应的程序。直到执行完毕后退出子进程,父进程回收子进程的资源。
代码如下:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
// 保存切割后的命令字符串
char *g_argv[SIZE];
// 保存完整的命令行字符串
char cmd_line[NUM];
char g_myval[64];
int main()
{
// 0.命令行解释器,一定是一个常驻内存的进程,不退出
while(1)
{
// 1. 打印提示信息
// [chakming@localhost test_09_22]$
// char buf[NUM] = {0};
// getcwd(buf,sizeof(buf));
printf("[chakming@localhost myshell]$ ");
fflush(stdout);
// 2. 获得用户的键盘输入[指令和选项]
memset(cmd_line,'\0',sizeof(cmd_line));
// fgets
if(fgets(cmd_line,sizeof cmd_line,stdin) == NULL)
continue;
// 去掉'\n'
cmd_line[strlen(cmd_line)-1] = '\0';
// printf("echo : %s\n",cmd_line);
// 3.切割输入的指令 "ls -l -a" -> "ls" "-l" "-a"
g_argv[0] = strtok(cmd_line,SEP);
int index = 1;
if(strcmp(g_argv[0],"ls") == 0) // 不能让子进程执行
{
g_argv[index++] = "--color=auto"; // 改变颜色
}
// 如果输入ll 相当于输入ls -l --color=auto
if(strcmp(g_argv[0],"ll") == 0)
{
g_argv[0] = "ls";
g_argv[index++] = "-l";
g_argv[index++] = "--color=auto"; // 改变颜色
}
// 第二次传入,如果是解析原始字符串,传入NULL
while(g_argv[index++] = strtok(NULL,SEP));
if(strcmp(g_argv[0],"export") == 0 && g_argv[1] != NULL)
{
strcpy(g_myval,g_argv[1]);
int ret = putenv(g_myval);
if(ret == 0) printf("%s export success\n",g_argv[1]);
continue;
}
// for(index = 0;g_argv[index];index++)
// printf("g_argv[%d]:%s\n",index,g_argv[index]);
// 4.TODO,内置命令,让父进程自己执行的命令,叫做内置命令
// 本质上就是shell函数调用
if(strcmp(g_argv[0],"cd") == 0) // 不能让子进程执行
{
if(g_argv[1] != NULL)
chdir(g_argv[1]);
continue;
}
// 5.fork()
pid_t id = fork();
if(id == 0)
{
execvp(g_argv[0],g_argv);
exit(1);
}
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret > 0) printf("exit code: %d\n",WEXITSTATUS(status));
}
}