什么是进程
概念:一个正在执行的程序,包含多个资源
{
标识符:用于区别于其他进程
状态:若进程正在执行,则进程处于执行态
优先级:相对于其他程序的优先级
程序计数器:程序中即将被执行的下一条指令的地址
内存指针:程序代码和进程相关数据的指针,还有和其他进程共享内存的指针
上下文数据:程序执行时处理器的寄存器中的数据
}
上述信息存放在进程控制块中(PCB) task_struct
操作系统是通过双向链表来组织所有PCB。
进程的运行状态
新建-就绪=运行-退出
| /
阻塞
进程的创建过程
写时拷贝技术:传统的fork()直接将所有资源复制给新创建的进程,这种简单但是效率低下,若新进程要立即执行新的映像那么前面的拷贝都是白费,写时拷贝是推迟甚至免除拷贝的技术,当创建新进程的时候首先让父子进程共享同一块内存,当某一方需要进行修改时,数据才会被拷贝,所以fork()开销仅仅是分配的唯一进程描述符。
fork()和vfork()的区别:vfork不会讲父进程的地址空间完全复制给子进程,当子进程调用exec或exit之前,子进程在父进程空间执行,故比fork效率高;vfork会保证子进程先运行,在子进程执行exit或exec后,父进程才会被调度执行。
步骤:
1.为新进程分配一个唯一的进程标识号。
2.给进程分配空间。
3.初始化进程控制块,进程标识号包括进程ID和其他相关ID,处理器状态信息初始化为0。
4.设置正确的连接,新进程处于就绪/挂起链表中
5.创建或扩充其他数据结构。
fork浅拷贝的原因:
https://blog.csdn.net/xuzhangze/article/details/79754853
进程切换(用户态到内核态的切换)
方式 | 原因 | 使用 | 例子 |
---|---|---|---|
中断 | 当前指令的外部执行 | 对异步外部事件的反应 | 完成一次I/O操作 |
异常 | 与当前的指令执行有关 | 处理一个错误或异常条件 | 缺页异常 |
系统调用 | 显示调用 | 调用操作系统函数 | open() |
僵死进程以及处理方式
产生原因:父进程未结束,子进程结束,并且父进程未读取子进程的退出状态,仅有子进程的主题空间被释放,但是PCB未释放。
处理方式:
1.父进程调用wait或waitpid获取子进程的退出状态,这样会导致父进程在wait或waitpid调用后阻塞运行,直至子进程退出。
2.父进程调用signal(SIGCHLD,SIG_IGN),来忽略SIGCHLD信号,这样子进程结束后会由内核释放资源。
3.对子进程的退出捕获他们的退出信号SIGCHLD,在信号处理函数调用wait或waitpid来释放他们的资源。
会对系统产生影响:不释放PCB则进程号会一直被占用,当系统有大量僵死进程时,会因为没有可用的进程号而无法产生新的进程。
孤儿进程
概念:父进程结束,子进程未结束。孤儿进程会被系统进程(init进程)接管,负责进程的资源释放。
会对系统产生影响:孤儿进程会由init进程进行正常的资源释放,所以不会对系统产生危害。
守护进程以及编程方式
概念:在系统启动时自启(init进程一般是守护进程的父进程),在系统关闭的时候终止,生存周期长,一般在后台运行,可通过ps -axj查看系统守护进程。
编程方式:
1.首先调用umask将文件模式创建屏蔽字设置为0。
2.调用fork(),然后使父进程退出(exit),实现了以下目的,第一,若守护进程是作为简单shell命令启动的,那么父进程的终止会使shell认为这条命令执行结束;第二子进程继承了父进程的组ID,保证了子进程不会是一个进程组的组长进程,这是为setsid调用做前提条件。
3.调用setsid创建新会话,①成为新会话的首进程②成为新进程组的组长进程③没有控制终端
4.将工作目录更改为根目录
5.关闭不需要的文件描述符,使守护进程不会再拥有从父进程继承来的某些文件描述符使用open_max或getrlimit函数获取最高文件描述符值,然后循环关闭到该值。
6.某些守护进程打开/dev/null 使其具有文件描述符0,1,2,这样任何一个试图读标准输入,写标准输出或标准出错的库例程不会产生任何结果。
守护进程示例:
#include "apue.h"
#include <syslog.h>
#include <fcntl.h>
#include <sys/resource.h>
void daemonize(const char *cmd)
{
int i, fd0, fd1, fd2;
pid_t pid;
struct rlimit rl;
struct sigaction sa;
//清除文件创建掩码
umask(0);
//获取最大数量的文件描述符
if(getrlimit(RLIMIT_NOFILE, &rl) < 0)
err_quit("%s: can't get file limit", cmd);
//成为失去控制终端的会话负责人
if((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if (pid != 0) /* parent */
exit(0);
setsid();
//确保未来的开放不会分配控制终端
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
if(sigaction(SIGHUP, &sa, NULL) < 0)
err_quit("%s: can't ignore SIGHUP");
if((pid = fork()) < 0)
err_quit("%s: can't fork", cmd);
else if( pid != 0 ) /* parent */
exit(0);
//将当前工作目录更改为root,这样我们就不会阻止卸载文件系统
if(chdir("/") < 0)
err_quit("%s: can't change directory to /");
// 关闭所有打开的文件描述符
if(rl.rlim_max = RLIM_INFINITY)
rl.rlim_max = 1024;
for(i = 0; i < rl.rlim_max; i++)
close(i);
//将文件描述符0,1和2附加到/dev/null
fd0 = open("/dev/null", O_RDWR);
fd1 = dup(0);
fd2 = dup(0);
// 初始化日志文件
openlog(cmd, LOG_CONS, LOG_DAEMON);
if(fd0 != 0 || fd1 != 1 || fd2 != 2)
{
syslog(LOG_ERR, "unexpected file descriptors %d %d %d", fd0, fd1, fd2);
exit(1);
}
}
转自《UNIX环境高级编程》第十三章
进程替换(exec函数族的使用)
功能:调用它并不会产生新的进程,一个进程一旦调用exec函数,它本身如同死亡了一样,系统会将代码段替换成新的程序代码,唯一保留一个进程的ID,对于系统来说还是同一个进程,只不过干的另一件事情。
注意点:在使用exec函数之前,首先使用fork函数创建一个子进程,子进程调用exec函数。
具体函数:
int ececv(const char *path, char *const argv[]);
通过路径名方式调用可执行文件作为新的进程映像。
int execl(const char path, const char arg,…);
与execv函数的用法类似,只是在传递argv参数的时候每个命令行参数都声明为单独的参数,最后要以NULL指针结束。
int execve(const charpathname,const charargv[], char *constebvp[]);
patrhnam是执行程序的路径名,参数argv、envp与main函数的argv、envp对应-系统调用函数
int execlp(const char *file, const char *argv,…);
与execl函数类似
int execvp(const char *file, const char *const argv[]);
与execv函数类似