Linux 进程管理与程序开发
进程是Linux事务管理的基本单元,所有的进程均拥有自己独立的处理环境和系统资源,进程的环境有当前系统状态及其父进程信息决定和组成。
- 进程环境及进程属性
- 进程管理及控制
- LINUX 特殊进程
7.1进程环境及环境
7.1.1进程资源
进程是Linux系统下用户层管理事物的基本单元,每个进程都有自己独立的运行空间。
为了更好的管理Linux所访问的资源,系统在内核头文件include/linux/sched.h中定义了结构体 struct task_struct 来管理每个进程的资源。
7.1.2 进程状态
虽然Linux操作系统是一个多用户多任务的操作系统,但对于单CPU系统来说,在某一个时刻,只能有一个进程处于运行状态,其他进程都处于其他状态,等待系统资源,各任务根据调度算法在这些状态之间不停地切换。但由于CPU处理速率较快,使用户感觉每个进程都完全独立拥有整个系统。
用户级进程拥有以下几种状态:
- 就绪
- 运行状态
- 等待状态(可以被中断)
- 等待状态(不可以被中断)
- 停止状态
- 僵死状态
7.1.3 进程基本属性
与进程相关的属性包括进程号(PID)、父进程号(PPID)、进程组号(PGID)。
1:进程号(PID)
进程号是系统维护的唯一标识一个进程的正数,进程号是无法再用户层修改的。在Linux系统中,系统的第一个用户进程为init进程,它的PID为1,其他的进程PID以此增加。
getpid函数 获取当前进程的PID 该函数在unistdd.h文件中声明
执行失败 -1 ,错误原因存储于errno中,pid_t类型其实就是int类型
2:父进程号(PPID)
任何进程(除init进程)都是由另一进程创建,该进程称为被创建进程的父进程,被创建的进程称为子进程,父进程号无法在用户层修改,父进程的进程号(PID)即为子进程的父进程号(PPID),用户可以通过调用getppid函数来获取当前进程的父进程号(PPID),其函数定义在/unistd.h文件中。
3: 进程组号(PGID)
在Linux系统中,每个用户都拥有用户号(UID)和用户组号(GUID);和用户管理一样,进程也拥有自己的进程号(PID)和进程组号(PGID)。进程组是一个或者多个进程的集合。
它们与同一作业相关联,可以接受来自同一终端的各种信号,每个进程组都有唯一的进程组号,进程组号可以在用户层修改。
用户可以通过调用 getpgid()函数来获得指定的进程的进程组号(PGID)。其函数定义在 unistd.h文件中
如果执行失败则返回 -1 ,错误原因存储在errno中。
每个进程组都可以有一个组长进程,组长进程的进程组号等于其他进程号。但组长进程可以先退出,即只要在某个进程中有一个进程存在,则该进程组就存在,与其组长进程是否终止无关。进程组的最后一个进程可以终止,或者装一道另一个进程组。
加入一个现有的组或者一个新的进程组的系统调用函数setpgid(),其声明如下:
int setpgid(pid_t pid,pid_t pgid );
4:会话
会话(session)是一个或多个进程组的集合。系统调用函数getsid用来获取某个进程的会话号SID
函数:extern _pid_t getsid(_pid_t _pid)
某进程的会话SID是可以修改的,函数setsid()用来创建新的会话。
函数:extern _pid_t setsid(void);
如果调用进程已经是一个进程组的组长,则此函数返回错误。为了杜绝这种情况的发生,通常先调用fork创建子进程,然后使其父进程终止,而子进程则继续,在子进程中调用此函数。
如果调用此函数的进程不是一个进程组的组长,则此函数会创建一个新会话:
- 该进程会变成新会话首进程(seesion leader),会话首进程是创建该会话的进程。
- 该进程成为一个新进程组的组长进程。新进程组PGID 是该调用进程的PID
- 该进程没有控制终端,如果在调用setsid之前该进程就有一个控制终端,那么这种联系也会被中断。
5: 控制终端
会话和进程组有以下一些特点:
- 一个会话可以有一个控制终端,建立与控制终端连接的会话首进程被称为控制进程。
- 一个会话中的几个进程组可被分成一个前台进程组合几个后台进程组,如果一个会话有一个控制端,则它有一个前台进程组。
- 无论何时键入终端的终端键(DELETE或Ctrl+C),都会将中断信号发送给前台进程组的所有进程;无论何时键入终端的退出键(Ctrl+\),都会将退出信号发送给前台进程组的所有进程。
7.1.4 进程用户属性
Linux 是权限有严格控制的操作系统,某个进程拥有的真实用户号(RUID)、真实用户组号(RGID)、有效用户号(EUID)、有效用户组号(EGID)信息。
在说道进程用户时,需要将文件的拥有者与拥有者组加以区别。
1:进程真实用户号(RUID)
对于进程而言,创建该进程的用户UID(执行此程序的用户)为此程序真实用户号RUID.可以通过getuid 函数来获取当前进程的真实用户号(RUID)
函数: extern _uid_t getuid(void)
此函数无参数,如果执行成功将返回当前进程的UID;如果执行失败则返回-1,错误原因存储在errno中。
2:进程有效用户号(EUID)
EUID 主要用于权限检查。
3:进程用户组号(GID)
创建进程的用户所在的组号为该进程的进程用户组号(GID)。可以通过调用getgid()函数来获得当前进程的真实用户组号。
函数:extern _uid_t getgid(void)
此函数无参数,如果执行成功将返回当前进程的GID;如果执行失败则返回-1 ,错误原因存储在errno中。
4:有效进程用户组号(EGID)
一般情况下,EGID和GID 相同,但是,当某可执行文件设置了setgid位,那么任何用户(包括root用户)运行此程序时,其有效用户组号EGID该为文件的拥有者所在组,其原来EUID类似。
函数:extern _uid_t getegid(void)
此函数无参数,如果执行成功将返回当前进程的EGID;如果执行失败则返回-1 ,错误原因存储在errno中。
7.2 进程管理及控制
在开发应用程序时,程序猿需要有效管理进程。常见的进程管理方式包括
- 创建进程
- 获取进程信息
- 设置进程属性
- 执行进程
- 退出进程
- 跟踪进程
7.2.1 创建进程
1:fork函数介绍
在Linux环境下,创建进程的主要方法是调用fork()函数。Linux下所有的进程都有init(PID为1)直接或间接创建。
函数:extern _pid_t frok(void);
此函数没有参数 返回值如下:
- 如果执行成功,在父进程中将返回子进程(新创建的进程)的PID,类型为pid_t,在子进程将返回0,以区别父子进程。
- 如果执行失败,则在父进程中返回-1,错误原因存储在errno中。
fork函数调用成功后,其子进程会复制父进程的几乎所有信息(除PID等信息),主发复制父亲进程的代码段、数据段、BSS、堆、栈、打开文件描述符。另外,子进程从父进程继承下列属性:实际用户/组号、有效用户/组号、环境变量、堆文件的执行时关闭标志,信号处理方式设置、信号掩码、当前工作目录、根目录、文件模式创建掩码、文件大小设置等。
2:创建子进程应用示例
子进程从创建后和父进程同时执行,竞争系统资源,子进程在执行位置为fork返回位置。
/*
============================================================================
Name : frok_example.c
Author : file_demo
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include
#include
#include
#include
int main(void) {
pid_t pid;
if((pid == fork())==-1){
printf("fork error");
}
printf("bye!\n");
return EXIT_SUCCESS;
}
以上可以看出,fork函数后代码在子进程中也被执行。实际上,其他代码也在子进程代码段中,只是子进程执行位置为fork返回位置,其之前的代码无法执行罢了。
/*
* fork_example2.c
*
* Created on: Mar 4, 2016
* Author: james
*/
#include
#include
#include
#include
int main(){
pid_t pid;
pid = fork();
if(pid == -1){
printf("fork process error\n");
}else if(pid == 0){
printf("fork process ok\n");
}else {
printf("main process\n");
}
return EXIT_SUCCESS;
}
3:对打开文件的处理
fork函数创建子进程后,子进程将复制父进程的数据段、BBS段、代码段、堆空间、栈空间、文件描述符,而对于文件描述符关联的内核文件表项,则采用共享的方式。
案例如下:
/*
* fork_example3.c
*
* Created on: Mar 4, 2016
* Author: james
*/
include stdio.h
include stdlib.h
include unistd.h
include sys/types.h
include string.h
include fcntl.h
include sys/stat.h
int main(){
pid_t pid;
int fd;
int i = 1;
int status ;
char *ch1 = "hello";
char *ch2 = "world";
char *ch3 = "IN";
if((fd = open("text.txt",O_RDWR|O_CREAT,0644)) == -1){
perror("parent open");
exit(EXIT_FAILURE);
}
if(write(fd,ch1,strlen(ch1) == -1)){
perror("parent write");
exit(EXIT_FAILURE);
}
if(( pid = fork() )== -1){
perror("fork");
exit(EXIT_FAILURE);
}else if (pid == 0){
i = 2;
printf("in child\n");
printf("i = %d\n",i);
if(write(fd,ch2,strlen(ch2))== -1)
perror("child write error\n");
return 0;
}else{
sleep(1);
printf("in parent\n");
printf("i = %d",i);
if(write(fd,ch3,strlen(ch3)) == -1)
perror("parent,write");
wait(&status);
return 0;
}
}
案例说明:
在程序对文件描述符操作中,父进程手续打开创建文件test.txt 文件,接着向该文件中写入ch1(hello)内容,然后父进行等待1秒让子进程完成写ch2(world),操作,1秒后,父进程再次写入数据ch3(IN)。由文件test.txt内容可以看出父子进程共同对一个文件操作,且写入数据不交叉覆盖,说明父子进程共享文件偏移。
对变量i,在子进程中进程了第2次赋值(i= 2),其结果为2,而父进程中i的值不变,显然,父子进程各子拥有这一变量的副本,互相不影响。
4:结合vfork测试全局数据段与BSS段使用策略
vfork()函数创建新进程时无需完全复制父进程的地址空间,如果派生的进程只执行exec()函数,则使用fork()从父进程复制到子进程的数据空间将不被使用。这样效率非常低,从而使得vfork非常有用。根据父进程数据空间的大小,vfork比fork可以很大程度上提高性能。cfork只在需要的时候复制,而一般采用与父进程共享所有的资源的方式处理。
其函数:extern _pid_t vfork(void);
vfork()在子进程环境中返回0,在父进程环境中返回子进程的进程号。
在执行过程中,fork()和vfork()函数有一定的区别,fork函数是拷贝一个父进程的副本,从而拥有自己独立的代码段、数据段、以及堆栈空间,即成为一个独立的实体。而vfork是共享父进程的代码以及数据段。
案例示例:
/*
* fork_vfork_example4.c
*
* Created on: Mar 4, 2016
* Author: james
*/
#include
#include
#include
#include
#include
int glob = 6;
int main(){
int var ;
pid_t pid;
var = 88;
printf("in beginning:\tglob = %d \tvar = %d\n",glob,var);
if((pid = vfork())<0){
perror("vfork");
exit(EXIT_FAILURE);
}else if(pid == 0 ){
//printf("in child ,modify the var:glob++ var++\n");
glob++;
var++;
printf("in child,printf the var == %d, glob == %d\n",var,glob);
_exit(0);
}else{
printf("in parent,printf the var == %d, glob == %d\n",var,glob);
return 0;
}
}
5:在子函数调用vfork创建子进程。
案例示例:
/*
* fork_return_example4.c
*
* Created on: Mar 4, 2016
* Author: james
*/
#include
#include
#include
#include
void test(){
int i =1;
pid_t pid;
pid = vfork();
if(pid == -1){
perror("vfork error");
exit(EXIT_FAILURE);
}else if (pid == 0){
printf("pid == 0 getpid = %d,getppid = %d\n",getpid(),getppid());
_exit(0);
}else{
printf("pid == other getpid = %d,getppid = %d\n",getpid(),getppid());
}
}
void fun(){
char buf[100];
printf("buf len = %d\n",sizeof(buf));
int i ;
for(i = 0;i<100;i++){
buf[i] = 0;
printf("fun pid getpid = %d,getppid ==%d\n",getpid(),getppid());
}
}
int main(){
pid_t pid;
test();
fun();
return EXIT_SUCCESS;
}
7.2.2 在进程中运行新代码
1:函数功能介绍及应用
在fork函数创建子进程后,如果希望在当前子进程中运行新程序,则可以调用exec系列函数。当进程调用exec系列函数中的任意一个时,该进程代码段,数据内容完全由新成效替代。因为调用exec并不是创建新进程,所以前后的进程号等相关信息并不发生变化。exec,只是用新程序替换了当前进程的正文、数据、堆和栈段。
execl 函数:
extern int execl(_const char *_path,_const char *_arg ,...);
execl() 用来执行参数path字符所代表的文件路径(绝对路径),第二个参数代表执行文件时传递的argv,最后一个参数必须是空指针。
案例示例:
/*
============================================================================
Name : exec_example.c
Author : file_demo
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include
#include
#include
#include
int main(void) {
pid_t pid;
if((pid = fork())<0 ){
perror("fork error\n");
}else if (pid == 0){
//
int exec_resp = execl("/bin/ls","ls","-l","/home",(char)0);
printf("exec_resp = %d\n",exec_resp);
_exit(0);
}else {
//
printf("");
}
return EXIT_SUCCESS;
}
execle函数声明:
extern int execle(_const char *_path,_const char *_arg,...)
execle 用来执行参数path 字符串所代表的文件路径(绝对路径),第二个参数代表执行文件时传递的是argv,最后一个参数必须指向一个新的环境变量数组,此数组即成为新执行程序的环境变量。
/*
* execle_example.c
*
* Created on: Mar 4, 2016
* Author: james
*/
#include
#include
#include
int main( int argc, char *argv[],char *env[]){
execle("/bin/ls","ls","-l","/home",(char *)0,env);
return EXIT_SUCCESS;
}
execlp()函数声明
extern int execlp(_const char *_file,_const char *_arg,...)
execlp()会从PATH环境变量所指的目录中查找符合参数file的文件名,找到后执行该文件,第二个参数代表执行文件时传递的argv[0],最后一个参数必须用空指针NULL。
案例示例:
/*
* execlp_example.c
*
* Created on: Mar 5, 2016
* Author: james
*/
include
include
include
int main(int argc,char *argv[]){
execlp("ls","ls","-l","/home",(char)0);
return EXIT_SUCCESS;
}
execvp() 函数声明
extern int execvp(_const char *_file ,char *_const _argv[]);
除以上函数外,system()以新进程方式运行一个程序。然后结束。system()函数用来创建新进程,并再次进程中运行新进程,知道新进程结束后,才继续运行父进程,子进程结束后,会返回退出状态,(如wait一样)。
其函数定义如下:
extern int system (_const char *_command);
案例示例:
/*
* system_example.c
*
* Created on: Mar 5, 2016
* Author: james
*/
#include
#include
#include
#include
int main(){
int status;
status = system("pwd");
if(!WIFEXITED(status)){
printf("abnormal exit\n");
}else{
printf("the exit status is %d\n",status);
}
return 0;
}
2:执行新代码对打开文件的处理
案例示例:
include
include
include
int main(int argc, char *argv[]){
int i;
int fd;
char *ptr = "hello world\n";
fd = atoi (argv[1]);
i = write(fd,ptr,strlen(ptr));
if(i<=0){
perror("write\n");
}
return EXIT_SUCCESS;
}
/*
* fcntl_example.c
*
* Created on: Mar 5, 2016
* Author: james
*/
include
include
include
include
include
include
int main(){
int fd ,status;
pid_t pid;
fd = open("text.txt",O_RDWR|O_CREAT,0644);
if(fd == -1){
perror("open error");
exit(EXIT_FAILURE);
}
printf("befor child process write\n");
system("cat text.txt");
pid = fork();
if(pid == -1){
perror("fork error");
exit(EXIT_FAILURE);
}else if(pid == 0){
char buf[128];
sprintf(buf,"%d",fd);
execl("./newcode","newcode","buf",(char)0);
}else{
wait(&status);
printf("after child_preccess write\n");
system("cat text.txt");
}
return EXIT_SUCCESS;
}
7.2.3 等待进程结束
进程与进程间要进程信息的状态的传递与交互,必须使用进程间通信机制,Linux使用函数wait()和waitpid()在父子进程间提供了简单的父子进程间同步机制。
1:wait 等待子进程结束
调用 wait() 函数的父进程将等待该进程的任意一个子进程结束后才继续执行,如果有多个子进程,只需要等待其中的进程。其函数定义在/use/include/sys/wait.h文件中。
函数声明为:extern _pid_t wait (_WAIT_STATUS _stat_loc);//wait 函数
如果等到任意一个子进程结束,将返回当前结束的子进程的PID,同时将子进程退出时的状态存储到“——stat_loc”变量中。如果执行失败则返回-1,错误原因存储在 errno 中。
案例示例:
/*
* wait_example.c
*
* Created on: Mar 5, 2016
* Author: james
*/
#include
#include
#include
int main(int argc, char *argv[]){
pid_t pid_one ,pid_wait;
int status;
if((pid_one = fork()) == -1){
perror("fork error");
}else if(pid_one == 0){
printf("my pid is %d\n",getpid());
sleep(1);
exit(EXIT_SUCCESS);
}else{
pid_wait = wait(&status);
if(WIFEXITED(status)){
printf("wait on pid:%d return value is;%4x\n",pid_wait,WEXITSTATUS(status));
}else if(WIFSIGNALED(status)){
//printf("wait on pid :%d return value is:%4x\n",pid_wait,WIFSIGNALED(status));
}
}
return EXIT_SUCCESS;
}
2:waitpid() 等待子进程结束
用户可以使用waitpid()函数来等待指定子进程(指定PID的子进程)结束。
其函数定义/usr/include/sys/Wait.h
extern _pid_t waitpid (_pid_t _pid,int *_stat_loc,int _options);
其中,
第一个参数为进程PID值,该值的设置范围如下:
- PID > 0 ,表示等待进程PID 为该PID 值的进程结束。
- PID =-1 ,表示等待任意进程结束
- PID = 0 ,表示等待与当前进程的进程组PGID一致的进程结束
- PID <-1 ,表示等待进程组PGID是此值的绝对值的进程结束
第二个参数为调用它的函数中某个变量地址,如果执行成功,则用来存储结束进程的结束状态。
第三个参数为等待选项,可以设置为0,亦可为WNOHANG和WUNTRACED 定义如下:
define WNOHANG 1
define WUNTRACED 2
如果OPTIONS 设置为WNOHANG,而此时没有子进程退出,将返回0,而返回子进程的PID,并获取子进程的状态于参数STAT_LOC中。
waitpid 与 wait 的使用方法类似,因此可以将wait示例中的wait语句换成以下代码:
pid_wait= waitpid(pid_one,&status,0);
7.2.4 退出进程
在LINUX 下,可以通过以下方式结束进程
- 向exit或_exit发布一个调用
- 在main函数中执行return
- 隐含的离开main函数
LINUX常用的退出进程函数有exit()、_exit()和on_exit()
1:abort 中止进程
abort 函数用来中止进程。函数声明
extern void abort (void)
2:atexit 退出进程
注册一个函数在exit退出时调用。函数声明
extern int atexit(void(* _func)(void))
3:on_exit正常退出进程
正常结束当前调用函数。其函数声明
extern int on_exit(void(*_func)(int status,void *arg)void *arg);
on_exit()用来设置一个程序正常结束前调用的函数,当程序通过调用exit()或者从main中返回时,参数func所指定的函数先被调用,然后才真正由exit()结束程序,参数arg指针会传给func函数。
如果执行成功则返回0,否则返回-1,错误原因存储在errno中。
案例示例:
/*
============================================================================
Name : exit_example.c
Author : file_demo
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include
#include
void test_exit(int status ,void *arg){
printf("before exit()\n");
printf("exit %d\n",status);
printf("arg = %s\n",(char *)arg);
}
int main(void) {
char *str = "test";
on_exit(test_exit,(void *)str);
exit(4321);
return EXIT_SUCCESS;
}
4:exit退出进程
函数exit用于退出进程。其函数声明如下:
extern void exit(int _status);
exit()用来正常结束当前进程的执行,并把参数status返回给父进程,而进程所有的缓冲区数数据会自动写回并关闭文件。
如果执行成功没有返回值,否则返回-1 ,失败原因存储在errno中。
5:exit 与 return 区别
C 语言关键字与函数,exit()在main函数退出时有相似之处,但两者本质的区别:
- return退出当前函数主体,exit()函数退出当前进程,因此,在main函数里面return(0)和exit(0)完成一样的功能。
- return仅仅从子函数中返回,而子进程用exit()退出,调用exit()时要调用一段终止处理程序,然后关闭所有的IO流
6:_exit 不调用任何注册函数退出进程,其函数声明如下:
extern void _exit(int _status)
_exit()用来正常结束当前进程的执行,把参数status返回给父进程,并关闭文件。此函数调用后不会返回,而传递SIGHLD信号给父进程,父进程可以通过wait函数取得获得子进程的结束状态,exit()不会处理标准的IO缓冲区,如果要更新需要调用exit().
7.2.5 修改进程用户相关信息
1:access 核实用户权限
此函数用来检查真实用户号(UID)或真实用户组号(GID)是否拥有对文件的相应访问权限。
函数定义:extern int access (_const char *_name ,int type);
此函数的第一饿参数为欲访问的文件(需要包含路径),第二参数为相应的访问权限,文件权限定义如下:
- define R_OK 4
- define W_OK 2
- define X_OK 1
- define F_OK 0
如果文件具有测试权限,此函数将返回0,否则返回-1,其错误状态主要有以下几个:
- EACCESS :不具有指定的访问权限
- ENOENT :文件不存在
- EROFS :在只读文件系统要求写权限。
案例示例:
/*
* access_example.c
*
* Created on: Mar 5, 2016
* Author: james
*/
include < stdio.h >
include < stdlib.h >
include < unistd.h >
int main(){
int i ;
if(i == access("/etc/exports",X_OK) == -1){// 检查是否有执行的检验
perror("access");
exit(EXIT_FAILURE);
}else{
printf("the user have write permission\n");
}
return 0;
}
2:设置进程真实用户RUID
如果要显示修改此值,可以调用setuid函数。
函数声明
extern int setuid(_uid_t _uid);
此函数有一个参数,即设置进程真实用户号(RUID):
- 如果当前用户是超级用户,则将设置真实用户号(UID)、有效用户号EUID为指定ID,并返回0 以标识成功。
- 如果当前用户是普通用户,且设置UID值为自己的UID,则可以修改成功,否则无权修改,函数江返回-1
案例示例:
/*
* setuid_exp.c
*
* Created on: Mar 5, 2016
* Author: james
*/
#include
#include
#include
int main(){
int uid,euid,suid;
getresuid(&uid,&euid,&suid);
printf("uid = %d,euid = %d,suid = %d\n",uid,euid,suid);
printf("after setuid(501)\n");
setuid(501); //执行此程序 普通用户无法修改
uid = 1; euid = -1; suid = -1;
getresuid(&uid,&euid,&suid);
printf("uid == %d,euid = %d,suid = %d\n",uid,euid,suid);
return 0;
}
3 设置进程有效用户EUID
由前面可知,某个进程的EUID首先由该可执行文件的权限决定,如果该可执行文件设置setuid位,则EUID为执行此进程的用户;如果该可执行文件设置setuid位,则EUID为该可执行文件的拥有者。
函数setuid 用来设置有效用户号(EUID)
函数声明
extern int seteuid(_uid_t uid);
- 如果是超级用户,将设置有效用户号(EUID)为指定ID。如果调用成功,该函数将返回0;如果调用失败,将返回-1 并有以下错误代码设置。
- 如果是普通用户,可以设置EUID为自己的ID,如果想设置为其他用户则不予更改,将返回失败,此外,还可以调用setegid来设置音效egid
setegid 函数可以用来设置有效组ID,原理与seteuid类型,函数声明如下:
extern int setegid(__gid_t ,_gid)
案例示例:
/*
* setuid_exp.c
*
* Created on: Mar 5, 2016
* Author: james
*/
#include
#include
#include
int main(){
int uid,euid,suid;
getresuid(&uid,&euid,&suid);
printf("uid = %d,euid = %d,suid = %d\n",uid,euid,suid);
printf("after setuid(501)\n");
setuid(501);
setreuid(0,500);
uid = 1; euid = -1; suid = -1;
getresuid(&uid,&euid,&suid);
printf("uid == %d,euid = %d,suid = %d\n",uid,euid,suid);
return 0;
}
7.2.6 进程调度管理函数
每个进程都受调度策略和优先级的控制。这些参数可以通过应用程序显式执行 sched_setscheduler()或通过sched_setparam()函数来指定(这些函数可以按进程级别为多进程更改调度参数)。
优先级范围与每种策略相关。每种策略的优先级范围可以(但不需要)与其他策略的优先级范围重叠。选择要运行的进程时,将选择位于最高优先级非空进程列表首位的进程,然后,该进程会从其他进程列表中删除。
在常见操作系统中,调度算法主要有以下原则:
- FIFO 先入先出原则:首先请求服务的对象首先得到CPU的处理;
- 最短作业优先原则:需要最小系统时间的服务首先得到处理;
- 最高优先级优先原则:优先级最高的服务首先得到处理;
- 时间轮片原则;每个任务分配一个系统时间片,轮流执行;
1: 设置调度策略
设置调度策略sched_setscheduler 函数声明:
extern int sched_setscheduler (_pid_t __Pid,int policy,_const struct sched_param *__param);
sched_setscheduler()函数将pid所指定进程的调度策略和调度参数分别设置为param指向的sched_param结构中的policy及其参数。param结构中的sched_priority成员的值可以为任何整数,该整数位于policy所指定调度策略的优先级范围内(含边界值)。policy参数的可能值在头文件
- 如果存在pid所描述的进程,将会为进程ID等pid的进程设置调度策略和调度参数。
- 如果pid为零,将会为调用进程设置调用策略和调度参数。
- 如果进程pid含多个进程或轻量进程(即该进程时多进程的),此函数将会影响进程中各个内核可调用实体的策略和优先级,具体取决于调度争用范围。
对于使用系统争用范围创建的进程,此系统调用将不会对进程或基础内核调度实体的调度产生影响。
对于使用进程争用范围的进程,进程的调度参数不会受到影响。但是进程争用范围进程的基础内核调度实体会将其调度参数改为参数中指定的值,进程争用范围进程使用的内核调度实体(在此调用完成之后创建)从进程继承调度策略和相关调度参数。
返回的优先级为目标进程的旧优先级,但是如果使用某些其他接口更改各个进程或轻量进程的优先级,各个进程或轻量进程可能具有其他值。
更改其他进程的调度参数需要有相应的特权。调用进程必须具有相应的特权,或者是具有PRIV_STSCHED权限的组的成员,才能够调用sched_setscheduler();
/*
============================================================================
Name : sched_setscheduler.c
Author : sched_setscheduler
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include
#include
#include
int main(void) {
struct sched_param param;
int maxpri;
maxpri = sched_get_priority_max(SCHED_FIFO);// 获取最大值
if(maxpri == -1){
perror("sched_get_priority_max() failed");
exit(1);
}
param.sched_priority = maxpri;
if(sched_setscheduler(getpid(),SCHED_FIFO,¶m) == -1){//设置优先级
perror("sched_setscheduler() failed");
exit(1);
}
return EXIT_SUCCESS;
}
2:获取调度策略
获取调度策略sched_getscheduler ()函数声明如下:
extern int sched_getscheduler(_pid_t __pid)
sched_getscheduler()函数返回pid所指定进程的调度策略。
- 如果存在pid所描述的进程,将返回进程ID 等pid的进程的调度策略。
- 如果pid为零,将返回调用进程的调度策略
- 如果pid包含多个进程或轻量进程,此函数只返回进程调度策略和优先级。目标进程中的各个进程或轻量进程具有其自身的调度策略和优先级,它们可能与进度的调度策略和优先级不同。
3:设置调度参数
可以使用sched_getparam 设置调度参数,其函数声明如下:
extern int sched_getparam(__pid_t __pid,struct sched_param *_param);
sched_setparam函数将pid指定进程的调度参数设置为param指向的sched_param结构指定的值。
sched_param结构定义如下:
struct sched_param{
int sched_priority;
}
param 结构中的sched_priority成员的值可以为任何整数,该整数位于pid指定进程当前调度策略的优先级范围内(含边界值),该优先级的数值越大表示优先级越高(越强)。
如果存在pid所描述的进程,并且调用进程具有权限,将为进程ID等于pid 的进程设置调度参数。如果pid为零,将为调用进程设置调度参数。
如果进程的pid包含多个进程或轻量进程,此函数将会影响进程中各个内核可调度实体的策略和优先级,具体取决于调度争用范围。
- 对使用系统争用范围创建的进程,此系统调用不会对进程或基础内核调度实体的调度产生影响。
- 对于使用进程争用范围的进程,进程的调度参数不会受到影响。但是进程争用范围进程的基础内核调度实体会将其调度参数改为参数中指定的值。进程争用范围进程使用的内核调度实体(在此调用完成之后创建)从进程继承调度策略和相关调度参数。
- 对于单进程的进程,其调度参数也将会更改。
只有超级用户才能更改其他进程的调度参数。调用进程必须具有相应的特权。
4:读取调度参数
可以使用sched_getparam 设置调度参数:
extern int sched_setparam(_pid_tm_const struct sched_param *_param);
sched_getparam()函数返回进程的调度参数(该进程由param指向sched_param 结构中pid指定)。
5:放弃进程
extern int sched_yield(void)
sched_yield()函数强制调用进程放弃处理器,直到调用进程再次位于进程列表的首位。该函数不带参数。
6:获取优先级值的范围
sched_get_priority_max()和 sched_get_priority_main()函数分别为algorithm指定的调度策略返回的相应最大值和最小值。
7:获取进程的执行时间限制
8:获取进程优先级
9:设置进程优先级
7.3Linux特殊进程
7.3.1守候进程及其创建过进程
1:守候进程的特点
守护进程(Daemon)是在后台进程的一种特殊进程,脱离于终端。
一般情况下,守护进程可以通过以下方式启动:
- 在系统启动时由脚本启动,这些启动脚本通常放在/etc/rc.d目录下
- 利用inetd超级服务器启动,如telnet等。
- 由cron定时启动以及在终端nohup启动进程也是守护进程。
2:守护进程编程要点
下面是编写守护进程的基本过程:
- (1)屏蔽一些有关控制端的信号,这是为了防止在守护进程没有正常运行起来前,控制终端受到干扰退出或挂起:
- (2)在后台运行。 这是为了避免挂起控制终端将其放入后台执行。方法是在进程中调用fork使其fork使父进程终止,让其子进程中后台执行。
- (3)脱离控制终端和进程组,应为进程属于一个进程组,进程组号(PGID)就是进程组长的进程号(PID)。同进程组中进程共享一个控制终端,这个控制终端通常是创建进程的shell登陆终端。而控制终端和进程组通常是父进程继承下来的,需要摆脱它们,使之不受它们的影响。因此需要调用setsid使子进程成为新的会话组长
案例示例:
setsid调用成功后,调用进程会成为新的会话组长和新的进程组长,并于原来的登录会话和进程组脱离,由于会话过程对控制终端的独占性,进程同时与控制终端脱离。 - (4)禁止进程重新打开控制终端。
- (5)关闭打开的文件描述符。
- (6)改变当前工作目录。
- (7)重设文件创建淹模。
- (8)处理SIGGHLD信号(子进程退出信号)。但对于某些进程,特别是服务器进程往往在请求到来时生成子进程处理请求。如果父进程不等待子进程结束,子进程将成为僵尸进程(zombie)从而占用系统资源。
如果父进程等待子进程结束,将增加父进程的负担,影响服务器进程的并发性能。在Linux下可以简单地将SIGCHLD信号的操作设为SIG_IGN来解决这一问题。
示例: signal(SIGCHLD,SIG_IGN)// signal函数的使用
这样,内核在子进程结束时不会产生僵死进程。
7.3.2日志信息及其管理
1:日志信息基本概念
为了告诉系统管理员守护进程的运行情况,守护进程需要输出特定信息,而守护进程又不能把信息输出到某个终端,因此,守护一般采用日志信息的方式输出。
2:建立与日志守候进程联系
在进程中,调用函数openlog()将与日志守护进程建立联系,也就是说,如果需要写日志信息,一般情况下,都需要显示调用此函数,告诉日志守护进程当前进程将写日志;
extern void openlog(__const char *_ident,int_option,int _facility)
第一个参数:要向每个消息加入字符串,一般可设置为当前进程名
第二个参数:用来描述一打开选项
第三个参数:消息的类型,决定将消息写入到那个日志文件中。
如果在关闭与日志守护进程的联系,可以调用colselog函数
extern void closelog(void)
3: 写日志信息
syslog()将产生一条日志信息,然后由日志守护进程将其发布到个日志文件中,该函数声明:
extern void syslog(int _pri ,__const char *_fmt,...);
第一个参数决定日志类别.
第二个参数为日志输出格式
7.3.3守候进程应用示例
/*
============================================================================
Name : Daemon_exp.c
Author : james
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
int init_daemon(const char *pname,int facility){
int pid;
int i;
signal(SIGTTOU,SIG_IGN);//处理可能的终端信号
signal(SIGTTIN,SIG_IGN);
signal(SIGTSTP,SIG_IGN);
signal(SIGHUP,SIG_IGN);
if(pid == fork()){// 创建子进程,父进程退出
exit(EXIT_SUCCESS);
}else if(pid < 0){
perror("fork");
exit(EXIT_FAILURE);
}
setsid();// 设置新会话组长,新进程组长,脱离终端
if(pid == fork()){// 创建新进程,子进程不能再申请终端
exit(EXIT_SUCCESS);
}else if(pid < 0){
perror("fork");
exit(EXIT_FAILURE);
}
for(i = 0;i < NOFILE;++i)// 关闭父进程的文件描述符
close(i);
open("/dev/null",O_RDONLY);// 对标准输入输入全部重定向到/dev/null
open("/dev/null",O_RDWR);// 因为先前关闭了所有的文件描述符,新开的值为0,1,2
open("/dev/null",O_RDWR);
chdir("/tmp");// 修改主目录
umask(0);//重新设置文件掩码
signal(SIGCHLD,SIG_IGN);// 处理子进程退出
openlog(pname,LOG_PID,facility);//与守候进程简历联系,加上进程号,文件名
return 1;
}
int main(int argc,char *argv[]) {
FILE *fd;
time_t ticks;
init_daemon(argv[0],LOG_KERN);
while(1){
sleep(1);
ticks = time(NULL);
syslog(LOG_INFO,"%s",asctime(localtime(&ticks)));
}
return EXIT_SUCCESS;
}
7.3.4:孤儿进程与僵死进程
孤儿进程:因父进程先退出而导致的一个子进程被init进程收养的为孤儿进程。
/*
============================================================================
Name : orphan_p.c
Author : james
Version :
Copyright : Your copyright notice
Description : Hello World in C, Ansi-style
============================================================================
*/
#include
#include
#include
int main() {
pid_t pid;
if((pid = fork()) == -1)
perror("fork");
else if(pid == 0){
while(1){
printf("pid = %d,ppid = %d\n",getpid(),getppid());
sleep(2);
printf("pid = %d,ppid = %d\n",getpid(),getppid());
}
}
else
exit(0);
}
僵死进程:而已经退出但还没有回收资源的进程为僵死进程。
案例示例:
/*
* dead_p.c
*
* Created on: Mar 7, 2016
* Author: james
*/
#include
#include
#include
int main(){
pid_t pid ;
if((pid = fork()) == -1){
perror("fork");
exit(EXIT_FAILURE);
}else if(pid == 0){
printf("child_pid pid = %d\n",getpid());
exit(0);
}
sleep(3);
system("ps");
exit(0);
}