基于C,进一步研究 linux内核函数,系统级别的函数
2017年8月
1. 知识了解
1.1 基本概念:
(1)程序 program
被存放在磁盘中的可执行文件。
(2)进程 process
是程序的执行实例。每个进程具有独立的权限和职责,运行在各自的虚拟地址空间中。
进程之间不会相互影响,但可以进行通信。
(3)进程ID,process ID简称PID
非负整数,进程的数字标识符。
1.2 启动例程
(1)“启动例程”被编译在main之前运行。
(2)收集命令行的参数,传递给main中的argc,argv。以及环境表envp。
(3)登记“终止函数”atexit()。
1.3 进程的终止
(1)正常终止
- 从main函数返回 (return)
- 调用exit(标准C库函数)
- 调用_exit或_Exit(系统调用)
- 最后一个线程从“启动例程”返回
- 最后一个线程调用pthread_exit
注意:
return和exit(),会刷新标准IO缓存,会自动调用终止函数。
_exit()和_Exit(),不刷新,也不调用。
(2)异常终止
- 调用abort
- 接收一个信号并终止
- 最后一个线程对“取消”请求 做出响应
(3)进程返回
- 通常程序成功返回0,否则返回非0
- 在shell中,可以产看进程返回值(echo $?)
(4)终止函数
#include <stdio.h>
int atexit(void (*function)(void));
向内核登记终止函数,成功返回0,否则-1
- 每个启动的进程都默认登记一个标准的终止函数
- 终止函数在进程终止时释放进程所占用的一些资源
- 登记多个终止函数,执行顺序以栈的方式执行,先登记后执行。
1.4 示意图
1.5 进程资源限制
linux中可用的资源resource如下:
RLIMIT_AS 进程可用的存储区大小
RLIMIT_CORE core文件最大字节数
RLIMIT_CPU CPU时间最大值
RLIMIT_DATA 数据段最大长度
RLIMIT_FSIZE 可创建文件的最大长度
RLIMIT_LOCKS 文件锁的最大数
RLIMIT_MEMLOCK使用mlock能否在存储器中锁定的最长字节数
RLIMIT_NOFILE 能打开的最大文件数
RLIMIT_NPROC 每个用户ID可拥有的最大子进程数
RLIMIT_RSS 最大驻内存集的字节长度
RLIMIT_STACK 栈的最大长度
- 头文件
#include <sys/resource.h>
struct rlimit{
rlim_t rlim_cur;/*软件限制:当前限制*/
rlim_t rlim_max;/*硬件限制:当前限制可以达到的最大值*/
}
- 函数
(1)获取进程的资源限制,存放在rlptr指向 的结构体中。成功返回0,失败非0。
int getrlimit(int resource , struct rlimit *rlptr);
(2)修改resource指定的资源限制,通过rlptr指向的结构体。成功返回0
int setlimit(int resource,const struct rlimie *rlptr);
- 配置文件
(1)/etc/security/limits.conf
(2)linux中,进程资源的初始化由0号进程建立,并被后续进程继承。 - 资源限制的修改规则
(1)硬件资源限制必须大于等于软件限制。
(2)任何一进程可以降低或者提升其软件资源限制,但必须大于其软件限制。普通用户不可逆此操作。
(3)超级用户可以提高硬件限制。
2. 与进程有关的指令
2.1 PS指令
可以查看到:进程ID(PID),进程的用户ID,进程状态STAT,进程的command等等。
3. 进程常见状态
3.1 运行状态
系统当前的进程
就绪状态进程
PS命令的stat列 == R
3.2 等待状态
等待事件发生
等待系统资源
PS命令的stat列 == S
3.3 停止状态
PS命令的stat列 == T
3.4 僵尸状态
进程终止或结束
在进程表项中仍有记录
PS命令的stat列 == Z
3.5 进程状态的变换关系
4. 进程的调度
4.1 一般性步骤
(1)处理内核中的工作
(2)处理当前进程
(3)选择进程(实时进程和普通进程)
(4)进程交换
4.2 task_struct中的调度信息
(1)策略
- 轮流策略
- 先进先出策略
(2)优先权
- jiffies变量
(3)实时优先权
- 实时进程之间
(4)计数器
5. 进程标识
进程有很多的标识:
当前进程ID,实际用户ID,有效用户ID,用户组ID,父进程ID,进程组ID。
与此相关的函数:
- 头文件
#include <unistd.h>
#include <sys/types.h>
- 获取进程标识的函数
pid_t getpid(void); //获取当前进程的ID标识
pid_t getppid(void); //获取父进程的ID标识
pid_t getpgrp(void); //获取当前进程所在的进程组ID标识。
pid_t getpgid(pid_t pid); //获取指定ID的进程所在的进程组ID标识。
uid_t getuid(void); //获取当前进程的 实际用户ID
uid_t geteuid(void); //获取当前进程的 有效用户ID
gid_t getgid(void); //获取当前进程的用户组ID
6. 进程的创建
本文的重点内容。
6.1 创建子进程的函数 fork
-
fork函数
fork创建的新进程被称为子进程。该函数被调用一次,会返回两次。
返回两次的区别:
(1)在父进程里,返回的是 新子进程的进程ID。
(2)在新子进程里,返回的是0。因为子进程的数据段、堆、栈都是重新创建的。
(3)父和子进程的运行顺序,根据系统调度自动决定。
(4)子进程复制父进程的内存空间。 -
vfork函数
与fork类似,但是 子进程先行运行,且不复制父进程的内存空间。 -
子进程的继承属性
- 用户信息和权限
- 目录信息,信号信息,环境,资源限制
- 共享存储段,堆,栈和数据段,共享代码段
- 子进程的特有属性
- 进程ID
- 锁信息
- 运行时间
- 未决信号
- 操作文件时内核结构变化
- 子进程继承文件描述表,不继承但共享文件表项和i-node。
- 创建一个子进程后,文件表项中的引用计数器加1变成2,当父进程作close操作后,计数器减1。子进程还是可以使用文件表项。只有当计数器为0时,才会释放文件表项。
6.2 进程寄生 exec函数簇
- exec函数用于执行另一个程序。新执行的程序会替换原进程的正文,数据,堆,栈。
- exec并不是创建新进程,前后的进程ID并没有改变。
- 在fork创建一个子进程之后,可以在子进程中使用exec函数执行另一个程序。
- 头文件
#include <unistd.h>
2.函数
//list 列出每个字符参数
int execl(const char *pathname,const char *arg0, ... /*(char*)0*/);
//argv 字符数组
int execv(const char *pathname,char * const argv[]);
//list 列出每个字符参数,环境表
int execle(const char *pathname,const char *arg0, ... /*(char*)0,char* const envp[]*/);
//argv 字符数组,环境表
int execve(const char *pathname,char * const argv[],char* const envp[]);
//
int execlp(const char *pathname,const char *arg0, ... /*(char*)0*/);
//
int execvp(const char *pathname,char * const argv[]);
上述所有的返回:出错返回-1 ,成功不返回。
6.3 system函数
- system函数,内部构件一个子进程,由子进程调用exec函数。
- 头文件
#include <stdlib.h>
2.函数
简化exec函数的使用,成功返回执行命令的状态,错误返回-1
int system(const char * command);
7. 几种特殊的进程
7.1 守护进程 daemon
- 守护进程是生存期很长的一种进程。他们常常在系统引导装入时启动,在系统关闭时终止。
- 所有守护进程都是以超级用户(用户ID为0)的优先权运行。
- 守护进程没有控制终端。
- 守护进程的父进程都是init进程。
7.2 孤儿进程
- 父进程结束,子进程就成了孤儿进程。由1号进程(init进程)领养。
7.3 僵尸进程
- 子进程结束,但是没有完全释放内存(在内核中的task_struct没有释放)。该进程就成了僵尸进程。
- 当僵尸进程的父进程结束,由1号进程(init进程)领养,最终回收。
- 如何避免僵尸进程:
(1)让僵尸进程的父进程来回收。父进程每隔一段时间来查询子进程是否结束并回收。调用wait()或者waitpid(),通知内核释放僵尸进程。
(2)采用信号SIGCHLD,通知处理。并在信号处理程序中调用wait()。
(3)让僵尸进程成为孤儿进程,由init进程来回收。
- 头文件
#include <sys/types.h>
#include <sys/wait.h>
- 函数
(1)等待子进程退出并回收。防止僵尸进程。成功返回子进程ID,出错返回-1。
pid_t wait(int *status);
(2)wait函数的非阻塞版本。成功返回子进程ID,出错返回-1。
pid_t waitpid(pid_t pid , int * status ,int options);
1)pid参数。
pid==-1 //等待任一子进程,功能和wait等效。
pid== 0 //等待 同进程组ID的任一子进程。
pid > 0 //等待指定的子进程
pid < -1 //等待 组ID等于(-pid)的任一子进程。
2)status参数。为空时,等待回收【任意状态】结束的子进程。不为空,则等待【指定状态】结束的子进程。
检查wait和waitpid函数返回终止状态的宏
WIFEXITED/WEXITSTATUS(status) //若为正常终止子进程返回的状态,则为真。
WIFSIGNALED/WTERMSIG(status) //若为异常终止子进程返回的状态,则为真。(接到一个不能捕捉的信号)
WIFSTOPED/WSTOPSIG(status) //若为当前暂停子进程返回的状态,则为真。
3)options参数。
WNOHANG //若由pid指定的子进程没有退出,则立即返回。waitpid不阻塞,返回值为0。
WUNTRACED //若某实现支持 作业控制,则有pid指定的任一子进程状态已暂停,其状态自暂停以来还没有报告过,则返回其状态。
- wait和waitpid函数的区别
(1)在一个子进程终止前,wait使其调用者阻塞。waitpid 不会阻塞。
(2)wait等待所有的子进程;waitpid等待指定的子进程。
8. 进程组
- 一个或者多个进程的集合
- 可以接收同一终端的各种信号。同组共信号。
- 唯一的进程组ID
- 组内所有的进程结束,进程组才会消亡。
- kill命令 发送信号给进程组。
8.1 获取进程组号
前文已经给出:
#include <unistd.h>
pid_t getpgrp(void); //获取当前进程所在的进程组ID标识。
pid_t getpgid(pid_t pid); //获取指定ID的进程所在的进程组ID标识。
8.2 组长进程
- 进程组的创建从第一个进程(组长进程)加入开始。组长进程的ID作为组ID。
- 组长进程可以创建进程组以及该组中的进程。
- 头文件
#include <unistd.h>
- 函数
(1) 将进程加入到指定的进程组中。成功返回0,错误返回-1。
int setpgid(pid_t pid,pid_t pgid);
(2) pid参数为指定的进程号,pgid为进程组。
8.3 前台进程组
- 在linux中,自动接收终端信号的组,成为前台进程组。
- 在终端中,CTRL+c 之类的信号,首先被前台进程组接收。
- shell启动的若干个进程组,默认父进程所在的组为前台进程组。
- 头文件
#include <unistd.h>
- 函数
(1)获取前台进程组ID。成功返回 前台进程组ID,错误返回-1。
pid_t tcgetpgrp(int fd);
(2)使用pgrpid,设置前台进程组ID
int tcsetpgrp(int fd,pid_t pgrpid);
(3)fd必须引用该会话的控制终端。0表示当前正在使用的终端。