Linux进程控制
进程 = 内核数据结构(struct task_struct,struct mm_struct,页表)+ 代码和数据
在Linux中fork函数时非常重要的函数,它从已存在进程中创建一个新进程。新进程为子进程,而原进程为父进程
1.进程创建
./程序
fork()创建子进程
2.fork函数
功能 | 创建一个新的进程 |
---|---|
头文件 | #include <unistd.h> |
原型 | pid_t fork(void); |
返回值 | 成功: 0 或者大于 0 的正整数,失败:-1 |
备注 | 该函数执行成功之后,将会产生一个新的子进程,在新的子进程中其返回值为0 ,在原来的父进程中其返回值为大于 0 的正整数,该正整数就是子进程的PID |
fork可以用来创建一个子进程,一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值,而父进程中返回子进程ID
进程调用fork,当控制转移到内核中的fork代码后,内核做:
- 分配新的内存块和内核数据结构给子进程
- 将父进程部分数据结构内容拷贝至子进程
- 添加子进程到系统进程列表当中
- fork返回,开始调度器调度
fork调用失败的原因
- 系统中有太多的进程
- 实际用户的进程数超过了限制
3.进程终止(退出码和退出信号)
进程终止做的事:释放曾经代码和数据占据的空间,释放内核数据结构
进程正常终止时会返回进程的退出码(异常除外,无退出码):
- 0:成功
- !0:失败-------1.2.3.4.5.6….不同数字值代表不同失败原因对应相应的string错误信息
正常终止(可以通过 echo $?
查看进程退出码)
进程终止的3种情况:
- 代码跑完,结果正确
- 代码跑完,结果错误
- 代码无法正确执行,出现异常(野指针…),这时候无法得到退出码,因此需要抛出异常
代码异常被操作系统终止本质是操作系统给进程抛出信号
例如: kill -l
如图所示退出信号
衡量一个进程退出,需要两个数字:退出码 + 退出信号!!
如何正常终止进程?
- 从main函数直接
return
返回 - 代码中调用
void exit(int status)
- _exit:
void _exit(int status);
exit()
和_exit()
前者会在进程退出时候冲刷缓冲区,后者不会
原因:前者是C库函数,后者是进行系统调用,而exit()实质上是调用_exit()这一个操作系统接口
如何异常终止进程?
kill
进程ctrl + c
任何子进程,在退出的情况下,一般必须被父进程进行等待
使进程退出的正确和错误方法:
- 正确的方法:
- 在main函数中调用return:这会导致程序正常退出,并返回值给操作系统。
- 在程序的任意位置调用exit接口:
exit
函数会终止当前进程,并将控制权返回给操作系统。exit
还会执行一些清理操作,比如调用注册的atexit
函数,关闭所有标准I/O流等。 - 在程序的任意位置调用_exit接口:
_exit
或_Exit
函数会立即终止调用进程,但与exit
不同,它不会执行任何清理操作,如关闭文件描述符,或刷新stdio缓冲区。
- 错误的方法:
- 在程序的任意位置调用return:
return
语句只能用在函数内部来结束函数的执行,并返回一个值。如果在main
函数中使用return
,它会导致程序退出,但如果在其他函数中使用,它只会结束当前函数的执行,并不会导致进程退出。
- 在程序的任意位置调用return:
4.进程等待的方法
wait
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int*status);
- 参数:
输出型参数,获取子进程退出状态,不关心则可以设置成为NULL
- 返回值:
成功则返回被等待进程pid
,失败:返回-1
waitpid
#include<sys/types.h>
#include<sys/wait.h>
pid_ t waitpid(pid_t pid, int *status, int options);
waitpid
函数参数的含义:
-
pid
:要等待的进程的ID。特定值有特殊含义: -
- 如果
pid
是-1
,waitpid
会等待任何子进程 - 如果
pid
大于0
,waitpid
会等待进程ID与之相匹配的特定子进程 - 如果
pid
等于0
,waitpid
会等待与调用进程相同进程组的任何子进程 - 如果
pid
小于-1
,waitpid
会等待进程组ID等于pid
绝对值的任何子进程
- 如果
-
status
:一个指向整数的指针,用于存储结束的子进程的状态信息。通过这个参数,调用者可以得知子进程是如何结束的(例如,是正常退出还是因为信号而结束) -
WIFEXITED(status)
: 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出,查看退出信号)WEXITSTATUS(status)
: 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
-
options
:修改waitpid
行为的选项。这些选项可以是以下几个值的组合: -
WNOHANG
:即使没有子进程退出,它也会立即返回(非阻塞模式)WUNTRACED
:除了返回终止的子进程信息外,还返回因信号而停止的子进程信息WCONTINUED
:返回那些已经停止(因为job control信号)并且已经继续执行的子进程状态
-
waitpid
函数的返回值是: -
- 如果成功,返回收集到状态信息的子进程的PID
- 如果设置了
WNOHANG
且没有子进程退出,返回0
- 如果出错,返回
-1
,并且errno
会被设置为相应的错误代码
获取子进程status
wait,waitpid
,都有一个status
参数,该参数是一个输出型参数,由操作系统填充- 如果传递
NULL
,表示不关心子进程的退出状态信息。否则,操作系统会根据该参数,将子进程的退出信息反馈给父进程 status
不能简单的当作整形来看待,可以当作位图来看待
进程等待实列:
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
int main()
{
pid_t id = fork();
if (id < 0)
{
perror("fork fail");
exit(-1);
}
// child run
if (id == 0)
{
printf("i am child process,pid = %d\n", getpid());
sleep(5);
// 子进程调用exit(0)来退出,并传递退出状态码0
exit(0);
}
// father run and write the child status
int status = 0;
//使用wait方法
pid_t ret = wait(&status);
if (ret == -1)
{
perror("wait child fail");
exit(-1);
}
// 如果进程正常退出,提取进程的退出码
if (WIFEXITED(status))
{
// WEXITSTATUS(status)提取进程退出码
// 当进程不是正常退出时候用WEXITSTATUS(status)是一种不可靠的行为
printf("wait child exit success,exit_code:%d\n", WEXITSTATUS(status));
}
else
{
printf("wait child exit fail\n");
}
return 0;
}
进程非阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t pid = fork();
if (pid < 0)
{
perror("fork fail");
exit(0);
}
else if (pid == 0)
{
// child
int cnt = 5;
while (cnt--)
{
printf("child is run, pid:%d,ppid:%d\n", getpid(), getppid());
sleep(1);
}
exit(0);
}
else
{
// father
int status = 0;
pid_t ret = 0;
do
{
ret = waitpid(-1, &status, WNOHANG); // 非阻塞式等待
if (ret == 0)
{
// 子进程还没结束
printf("Do father things\n");
}
sleep(1);
} while (ret == 0);
//
if (WIFEXITED(status) && ret == pid)
{
printf("wait child success, child exit code:%d\n", WEXITSTATUS(status));
}
else
{
printf("wait child failed\n");
return 1;
}
}
return 0;
}
进程阻塞等待
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
int main()
{
pid_t 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());
int cnt = 5;
while (cnt)
{
printf("child[%d] is running, cnt = %d\n", getpid(), cnt);
cnt--;
sleep(1);
}
exit(1);
}
else
{
//father
int status = 0;
pid_t ret = waitpid(-1, &status, 0); // 阻塞式等待
printf("wait child\n");
if (WIFEXITED(status) && ret == pid)
{
printf("wait child success, child exit code:%d\n", WEXITSTATUS(status));
}
else
{
printf("wait child failed,\n");
return 1;
}
}
return 0;
}
5.进程程序替换
替换原理:
用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数
以执行另一个程序。当进程调用一种exec函数
时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec函数
并不创建新进程,所以调用前后该进程的pid
并未改变
总结:老进程的壳子执行新程序的代码
替换函数:
其实有六种以exec开头的函数,统称exec函数
#include <unistd.h>
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 execve(const char *path, char *const argv[], char *const envp[]);
实质上前五种函数是系统调用接口execve
的封装
函数解释
- 这些函数如果调用成功则加载新的程序新的代码开始执行,不再返回到原代码中
- 如果调用出错则返回-1
- 所以exec函数只有出错的返回值而没有成功的返回值
命名理解
- l(list) : 表示参数采用列表
- v(vector) : 参数用数组
- p(path) : 有p自动搜索环境变量PATH
- e(env) : 表示自己维护环境变量