Linux系统编程学习笔记(五)进程管理

进程管理(一)

和文件一样,进程是Unix系统最基本的抽象之一。

1、进程ID:

每一个进程都有一个唯一的标示,进程ID。虽然进程ID是唯一的,但进程终止后,id会被其他进程重用。

许多UNIX都提供了延迟重用的功能,以防止新进程被误认为是旧进程。

有一些特殊的进程:
id为0的进程–idle进程或者叫做swapper,通常是一个调度进程。
id为1的进程–内核booting之后执行的第一个进程。init进程一般执行的是init程序。

Linux通常尝试执行以下init程序:
1、/sbin/init: 偏向、最有可能是init程序的地方。
2、/etc/init: 另一个很有可能是init程序的地方。
3、/bin/init: 有可能是init进程的地方。
4、/bin/sh:如果内核找不到init进程,就执行该bourne shell。
init进程是一个用户级进程,但是需要执行者有超级用户权限。

获得进程ID和父进程的ID:

#include <sys/types.h>  
#include <unistd.h>  
  
pid_t getpid(void);  
pit_t getppid(void);  

pid_t是个抽象类型,在linux中pid_t一般是一个int类型,在<sys/types.h>定义。
但把pid_t当做int类型,不具有可移植性。

例子:

printf("My pid=%d\n",getpid());  
printf("Parent's pid=%d\n",getppid());  

我们可以把pid_t比较安全的当做int类型,虽然这违反了抽象类型的意图和可移植性。

2、创建一个进程fork:

一个已经存在的进程可以通过fork创建其他进程:

#include <unistd.h>  
  
pid_t fork(void);  

新创建的进程被称为子进程,这个函数被调用一次但是被返回两次。在子进程返回0,父进程返回子进程的t_pid。
之所以在父进程返回子进程的id,是由于父进程可以有多个子进程,并且没有提供获得所有子进程的方法。在子进程中返回0,是因为子进程只有一个父进程,并且可以通过getppid获得。\

fork被调用之后,父进程和子进程都开始执行fork之后的程序语句。子进程是父进程的一个拷贝,拷贝了父进程的数据空间,堆,栈,它们共享text段。
当前fork的实现并不是拷贝父进程的数据、堆、栈,而是使用了copy-on-write技术,这是因为fork之后通常会调用exec。
如果它们修改了这些区域,那么内核就会把相应的那部分内存进行拷贝。

子进程和父进程有以下不同:
1)进程id不同
2) fork的返回值不同
2)子进程的父进程id设置为父进程的id,它们的父进程不同
3)子进程的资源统计归为0
4)任何pending的signals被清空,不会被子进程继承
5)任何获得的文件锁都不会被子进程继承。

相同的:
1)打开文件
2)real user ID,real group ID,effective user ID,effective group ID
3)进程的group ID
4)Session ID
5)控制终端
6)set-user-ID和set-group-ID
7)当前工作目录
8)Root目录
9)文件mode创建的掩码
10)信号掩码和dipositions
11)打开文件的close-on-exec flag
12)环境变量
13)附加进去的共享内存段
14)内存映像
15)资源限制

如果失败返回-1。

例子:

pid_t pid;  
  
pid = fork();  
if(pid > 0){  
    printf("I am the parent of pid=%d\n",pid);  
}else if(!pid){  
    printf("I am the baby!\n");  
}else if(pid == -1){  
    perror("fork");  
}  

fork经常和exec在一起使用:

pid_t pid;  
  
pid = fork();  
if(pid == -1)  
    perror("fork");  
  
if(!pid){  
    const char *args[] = {"windlass",NULL};  
    int ret;  
  
    ret = execv("/bin/windlass",args);  
  
    if(ret == -1){  
        perror("execv");  
        exit(EXIT_FAILURE);  
    }  
}  

使用fork的场景:
1)当一个进程想复制自己以便父子进程可以同时执行不同部分的代码。比如一个网络的服务器,父进程等待从客户端发来的请求,当请求到来时,父进程调用fork,让新创建的子进程处理请求,父进程继续等待客户端发来的请求。
2)当一个进程想执行不同的程序。比如shell,子进程在fork返回之后执行了exec。

3、vfork:

在copy-on-write技术使用之前,Unix的设计者认为fork之后执行exec浪费了地址空间的拷贝,BSD开发者实现了vfork系统调用:

#include <sys/types.h>  
#include <unistd.h>  
  
pid_t vfork(void);  

vfork和fork行为一样,除了子进程要立即调用exec函数或者执行exit退出。vfork系统调用避免了地址空间和页表的拷贝,通过挂起父进程直到子进程终止或者执行一个二进制的进程映像。

vfork的例子:

#include <sys/types.h>  
#include <unistd.h>  
  
int glob = 6;  
  
int main(){  
    int var;  
    pid_t pid;  
    var = 88;  
    printf("before vfork\n");  
    if((pid = vfork()) < 0){  
        perror("vfork");  
        return 1;  
    }else if(pid == 0){  
        glob++;  
        var++;  
        _exit(0);  
    }  
  
    printf("pid=%d,glob=%d,var=%d\n",getpid(),glob,var);  
    exit(0);  
}  

输出:pid=2903,glob=7,var=89.在子进程里面增加变量会反映到父进程中,因为他们共享数据段。

4、终止进程:

POSIX和C89都定义了终止当前进程的标准函数:

#include <stdlib.h>  
  
void exit(int status);  

调用exit会执行一些关闭操作步骤,然后指示内核终止进程。
status表示进程终止的状态。EXIT_SUCESS和EXIT_FAILURE被定义为一种可移植的方式来表示成功和失败。

在终止之前要做一些关闭的步骤:
1)调用任何注册在atexit()和on_exit()的方法,和注册的顺序相反。
2)flush所有打开的I/O流
3)删除进程由tmpfile()函数创建的临时文件。
执行完这些步骤之后,调用_exit(),让内核来处理剩余的终止操作:

#include <unistd.h>  
  
void _exit(int status);  

当进程终止后,内核清空了进程申请的所有资源。
程序可以直接调用_exit,但是很多程序需要执行一些清理操作,比如flush标准输出流。但是vfork用户必须使用_exit终止,因为父子进程共享一个地址空间,exit执行一些I/O清理工作可能把父进程的文件描述流关闭,导致父进程I/O失败。

5、atexit和on_exit:

1、atexit:注册在进程终止之前回调的函数:

#include <stdlib.h>  
  
int atexit(void (*function)(void));  

如果进程通过exit或者从main返回终止,则会调用注册到atexit的方法。如果进程调用exec函数,注册函数则被清空(因为这些函数不在新的进程空间存在)。如果信号终止了进程,则注册的函数不会被调用。
被注册的函数按照逆序执行,如果被注册的函数执行了exit,则会导致无穷递归,如果想提前终止需要使用_exit。atexit支持最多ATEXT_MAX个注册函数,这个值可以通过sysconf得到。

long atexit_max;  
  
atexit_max = sysconf(_SC_ATEXIT_MAX);  
printf("atexit_max=%ld\n", atexit_max);  

atexit例子:

#include <stdio.h>  
#include <stdlib.h>  
  
void out(void){  
    printf("atexit() succeed!\n");  
}  
  
int main(){  
    if(atexit(out))  
        fprintf(stderr,"atexit() failed!\n");  
    return 0;  
}  

2、on_exit:

on_exit和atexit等价,Linux glibc实现了它:

#include <stdlib.h>  
int on_exit (void (*function)(int , void *), void *arg);  

但是注册的签名函数不同,原型是:

void my_func(int status,void *args);  

status是传到exit的或者从main返回的值。args是传到on_exit的第二个参数。Solaris已经不再支持on_exit,所以最好使用atexit().

6、等待子进程终止:

当一个进程终止之后,内核向父进程发送一个SIGCHLD。默认这个信号被忽略,进程可以通过singnal()或者sigaction()系统调用来处理这个信号。父进程希望得到子进程终止的更多信息,比如返回值,甚至显式的等待这个事件的到来,这就是wait或者waitpid,它们可以做:
1)阻塞,如果子进程仍然在执行。
2)立即返回,包含子进程的终止状态,如果一个子进程终止,等待它的终止状态被获取。
3)返回错误,如果它没有子进程。\

#include <sys/wait.h>  
  
pid_t wait(int *statloc);  
pid_t waitpid(pid_t pid, int *statloc, int options);  

这两个函数的不同之处:
1)wait会阻塞调用者,直到一个子进程终止,而waitpid有一个设置不阻塞的选项。
2)waitpid不是等待第一个终止的子进程,他有一些选项来控制进程的等待。

如果一个进程终止,其父进程没有等待他终止,它就成为僵尸进程,僵尸进程的父进程会置为init进程,init周期性的调用wait来回收僵尸。
子进程的结束状态会保存在statloc指针中,如果不关心结束状态,可以直接传一个NULL。

POSIX指定了通过一系列宏来获得终止的状态。

#include <sys/wait.h>  
  
int WIFEXITED(status);  
int WIFSIGNALED(status);  
int WIFSTOPPED(status);  
int WIFCONTINUED(status);  
  
int WEXITSTATUS(status);  
int WTERMSIG(status);  
int WSTOPSIG(status);  
int WCOREDUMP(status);  

解释一下,IF就是如果,后面接子进程终止的情况。

WIFEXITED:如果子进程正常终止,返回true。可以通过WEXITSTATUS得到参数的低8位。
WIFSIGNALED:如果是信号导致子进程不正常终止,返回true。可以通过WTERMSIG返回信号的号。
一些UNIX实现定义了WCOREDUMP宏,如果进程dump core来响应信号。
WIFSTOPPED:如果进程被停止,返回true,通过WSTOPSIG来获得导致子进程停止的信号。
WIFCONTINUED:如果状态是由已经被continued子进程返回,返回true。

例子:

#include <unistd.h>  
#include <stdio.h>  
#include <sys/types.h>  
#include <sys/wait.h>   
  
int main(void){  
    int status;  
    pid_t pid;  
      
    if(! fork()){  
        return 1;  
    }  
  
    pid = wait(&status);  
    if(pid == -1)  
        perror("wait");  
    printf("pid=%d\n",pid);  
  
    if(WIFEXITED(status))  
        printf("Normal termination with exit status=%d\n",WEXITSTATUS(status));  
    if(WIFSIGNALED(status))  
        printf("Killed by signal=%d%s\n",WTERMSIG(status),WCOREDUMP(status));  
    if(WIFSTOPPED(status))  
        printf("Stopped by signal=%d\n",WSTOPSIG(status));  
    if(WIFCONTINUED(status))  
        printf("Continued\n");  
  
    return 0;  
}  

waitpid比wait功能更加强大。

pid参数指定了要等待的进程id:

< -1:等待任意一个绝对值和进程group id相等的进程。
-1:等待任意一个子进程,和wait一样
0:等待和当前进程相同组的任一子进程。
> 0: 等待该进程id的子进程

option 可以通过OR连接下列选项:
WNOHANG:不阻塞,如果没有匹配的子进程也直接返回。
WUNTRACED:如果被设置,WIFSTOPPED也会被设置。允许更一般的作业控制。
WCONTINUED:如果被设置,WIFCONTINUED也被设置。和WUNTRACED一起,对于实现一个shell很有用。

例子:

int status;  
pid_t pid;  
pid = waitpid (1742, &status, WNOHANG);  
if (pid == -1)  
    perror ("waitpid");  
else {  
    printf ("pid=%d\n", pid);  
    if (WIFEXITED (status))  
        printf ("Normal termination with exit status=%d\n",WEXITSTATUS (status));  
    if (WIFSIGNALED (status))  
        printf ("Killed by signal=%d%s\n",WTERMSIG (status),WCOREDUMP (status) ? " (dumped core)" : "");  
}  

7、exec函数:

当进程调用exec时,当前进程的镜像被由path标定的程序加载到内存中代替。下面是exec一族函数:

#include <unistd.h>  
  
int execl(const char *path,const char *arg,...);  
int execv(const char *path,char * const argv[]);  
int execle(const char *path,const char *arg,...);  
int execve(const char *path,char *const argv[], char * const envp[]);  
int execlp(const char *path,const char *arg,...);  
int execvp(const char *path, char *const argv);  

区别:
1)前四个参数path代表路径名,后两个代表文件名(不包含路径信息)。所以后两个多了1个p
如果文件名中包含反斜线,作为一个路径名。
如果否则从PATH环境变量中找可执行的文件。
如果execlp或者execvp找到了可以执行的文件,但是不是机器可执行的,那么就假设这是一个shell脚本,调用/bin/sh执行该shell脚本。
2)execl、execle、execlp使用的是参数列表,需要以NULL终止,而其他的几个带v(vector)的是一个数组参数,参数数组也要以NULL终止。所以参数列表是有l后缀,list。
3)execle、execve传递环境列表到新的程序中。所以有e后缀,表示environment,环境变量。

这里面只有execve是系统调用,其他都是函数。相当于包装了execve

例子:

int ret;  
  
ret = execl("/bin/vi","vi",NULL);  
if(ret == -1)  
    perror("execl");  

下面一个例子,要使用vi打开以及文件编辑:

int ret;  
  
ret = execl("/bin/vi","vi","/home/fuliang/books.txt",NULL);  
if(ret == -1)  
    perror("execl");  

成功调用execl之后,改变的不仅是地址空间和进程映像,而且还改变进程一下属性:
1)任何pending signals都被丢弃。
2)任何要捕获的信号都置成默认的行为。因为信号处理函数不在该进程的地址空间了。
3)任何的内存锁都被释放。
4)很多线程属性被设置为默认值。
5)任何和进程内存相关,包括内存映射文件,都被丢弃。
6)任何在用户空间存在的包括C语言库,比如atexit的行为被丢弃。\

没有改变的进程属性:进程pid,父进程id,优先级,进程所属的用户和组。

execvp例子:

const char *args[] = {"vi","/home/fuliang/books.txt",NULL};//参数vector 
int ret;  
  
ret = execvp("vi",args);//p,可执行文件名  
if( ret == -1 )  
    perror("execvp");  

失败返回-1,并设置errno:
1)E2BIG:参数列表或者环境变量envp太长。
2)EACCESS:进程没有搜索path的权限,path不是一个普通文件,目标文件不可执行,文件系统被mounted为不可读。
3)EFAULT:所给的指针不合法。
4)EIO:底层的I/O发生错误。
5)EISDIR:path或者解释器是一个目录。
6)ELOOP:解析path的时候遇到太多的软链。
7)EMFILE:调用进程超过了打开文件的限制。
8)ENFILE:系统打开文件数目超过限制。
9)ENOENT:path不存在,或者以来的共享库不存在。
10)ENOEXEC:目标path是一个不合法的二进制文件或者是不同的机器架构。
11)ENOMEM:没有足够的内核内存来执行新的程序
12)ENOTDIR:path不是一个目录.
13)ETXTBSY:目标文件正被其他的进程写

8、Launching and Waiting for a New Process:

ANSI C和POSIX都定义了一个接口,他结合了创建一个进程并且等待它的结束:

#include <stdlib.h>  
  
int system(const char *command);  

system一般用于执行一个简单的工具或者shell脚本。
成功返回命令的执行状态,如果command是NULL,则返回非0整数。
在执行command命令式,SIGCHILD被阻塞,SIGINT和SIGQUIT被忽略。忽略SIGINT和SIGQUIT有几个含义,尤其system在一个循环里被执行,这时你需要保证程序检查子进程的状态。

do{  
    int ret;  
      
    ret = system("ls -l");  
    if(WIFSIGNALED(ret) && WTFERMSIG(ret) == SIGINT || WTERMSIG(ret) == SIGQUIT) 
        break;  
}while(1);  

使用fork、waitpid简单实现system:

int my_system(const char *cmd){  
    int status;  
    pid_pid;  
  
    pid = fork();  
    if(pid == -1);  
        return -1;  
    else if(pid == 0){  
        const char *argv[4];  
        argv[0] = "sh";  
        argv[1] = "-c";  
        argv[2] = cmd;  
        argv[3] = NULL;  
        execv("/bin/sh",argv);  
        exit(-1);  
    }  
  
    if( waitpid(pid, &status,0) == -1)  
        return -1;  
    else if(WIFEXITED(status))  
        return WEXITSTATUS(status);  
  
  
    return -1;  
}  

进程管理2

1、用户和组:

进程是和用户和组关联的,用户id和组id分别是C语言的uid_t和gid_t类型表示。数字表示和可读字符串之间的映射关系是通过用户空间的/etc/passwd和/etc/group两个文件完成的,内核只处理数字表示形式。

在Unix系统中,一个进程的用户ID和组ID代表这个进程可以执行哪些文件操作以及向其他进程发送信号的能力。进程必须以合适的用户和组运行,最好的方式是遵循“最小权限”的原则。这个要求是变化的:如果进程在前期需要以root用户的权限运行,那么后面不需要root权限了,那么就应该在后面尽可能采用权限更小的用户权限运行。

1)一个进程的 real user ID 是指运行此进程的用户角色的 ID。就是最朴素意义上的login ID。
2)一个进程的 effective user ID 是指此进程目前实际有效的用户 ID(也就是权限的大小),effective user ID 主要用来校验权限时使用,比如打开文件、创建文件、修改文件、向别的进程发信号,等等。就是权限ID。

如果一个进程是以 root 身份来运行的,那么上面这两个 ID 可以用 setuid/seteuid 随便修改,但是如果一个进程是以普通用户身份来运行的,那么上面这两个 ID 一般来说是相同的,并且也不能随便修改。只有一种情况例外:此进程的可执行文件的权限标记中,设置了“设置用户 ID”位!

3)设置用户id是文件权限的一个标记,可以通过

chmod +s /path/to/file

来设置用户id位。一旦用了这个命令之后,再执行这个文件,那么生成的进程的 effective user ID 就变成了这个可执行文件的 owner user ID(属主用户 ID)。就是一个标志,可以用文件的拥有者权限执行该文件。

4)保存的设置用户id(saved set-user-ID),是程序在执行exec一个setuid程序(设置了设置用户id标记位的文件)之前的有效用户id,这样可以保存该保存的设置用户id来恢复之前的有效用户的id。当一个用户登录的时候,login程序会把真实的用户ID和有效的用户ID设置成/etc/passwd文件中指定的UID,当一个进程fork的时候,子进程会从父进程那继承它。真实UID和真实GID标识进程的真实所有者,会影响到发送信号的权限。相当于就是字面意思,存起来旧的权限ID。

2、改变实际用户(组)ID 和保存设置的用户(组)ID

#include <sys/types.h>  
#include <unistd.h>  
  
int setuid(uid_t uid);  
int setgid(gid_t gid);  

setuid是用来设置当前进程的有效id。如果当前用户的有效id是root,那么实际用户id和保存设置的用户id也会被同时设置为uid,root用户可以为uid提供任何值,从而将所有三种用户id都设置成uid了。非root用户只允许将实际用户id和保存设置的用户id作为uid的实际参数传递,也就是有效用户id只能是这两种的一个值。

成功返回0,错误返回-1,并设置errno:
EAGAIN: uid的值和实际用户id的值不同,把uid设置为real user id会让用户超过他的NRPOC限制(它指定了一个用户可以拥有的进程数)。
EPERM:用户不是root,uid既不是有效也不是保存用户ID。

上面的讨论也适合setgid。

3、改变有效用户或者组id:

Linux提供了两个POSIX所定义的函数来改变当前进程的有效用户id和组id的值:

#include <sys/types.h>  
#include <unistd.h>  
  
int seteuid(uid_t euid);  
int setegid(gid_t egid);  

seteuid是将有效用户ID的值设置为euid。root用户可以为euid提供任何值,而非root用户只能将有效用户ID设置为实际用户 ID或者保存设置的用户ID。成功返回0,失败返回-1,并且把errno设置为EPERM,它代表当前进程的所有者不是root用户,并且euid 既不等于用户ID也不同于实际用户id也不等于保存用户id(就是保存设置的用户ID)。 非root用户,seteuid和setuid的行为一样。始终使用seteuid()是一个标准实践和好的方法,除非你的进程倾向于以root身份运行, 这样setuid会变得更有意义。

4、获得用户和组ID:

以下两个系统调用返回真实用户和组的ID:

#include <unistd.h>  
#include <sys/type.h>  
  
uid_t getuid(void);  
gid_t getgid(void);  

相应的以下两个系统调用返回有效用户和组的ID:

#include <unistd.h>  
#include <sys/type.h>  
  
uid_t geteuid(void);  
gid_t getegid(void);  

这两组系统调用不会失败。

5、会话和进程组:

每个进程都属于某个进程组,进程组是由一个或者多个相互间有关联的进程组成,它的目的是为了作业控制。 进程组的主要特征是信号可以给进程组中的所有的进程:这个信号使同一个进程组中的所有进程终止、停止 或者继续运行。

每一个进程组都由进程组ID(pgid)唯一来标识,并且有一个组长进程(process goup leader),进程组的ID 就是组长进程的pid。只要在某个进程组中还有一个进程存在,则该进程组就存在,即使组长进程终止了,该进程 仍然存在。

当新的用户登录计算机时,登录进程就会为这个用户创建一个新的会话。这个会话中只有用户登录shell这一个进程。 登录shell作为会话首进程(session leader)。会话囊括了登录用户的所有活动,并且分配给用户一个控制终端。

1)与会话相关的系统调用:

在登录时,shell会创建新的会话。这是通过以下系统调用完成的:

#include <unistd.h>  
  
pid_t setsid(void);  

setsid创建的新会话,并在其中创建一个新的进程组,而且调用进程称为新会话的首进程和进程组的组长进程。
成功返回,新会话的会话ID,错误时,返回-1,并把errno设置成EPERM,表示调用进程是当前进程组的组长进程。
有一个简单的方法可以使得任何进程都不成为组长进程:创建一个新进程,终止父进程,让子进程来调用setsid。

例如:

pid_t pid;  
pid = fork();  
if(pid == -1){  
    perror("fork");  
    return -1;  
}else if(pid != 0){  
    exit(EXIT_SUCESS);  
}  
  
if(setsid() == -1){  
    perror("setsid");  
    return -1;  
}  

获得当前进程的会话id,虽然不怎么常用:

#include <unistd.h>  
  
pid_t getsid(pid_t pid);

调用成功返回进程会话ID。如果参数是0,则返回调用进程的会话id。
错误返回-1,并设置errno为ESRCH,表示pid不代表任何进程。
其他的UNIX系统可能设置errno为EPERM,它表示pid指示的进程和调用进程不属于同一个会话。Linux倾向于返回任何进程的会话id。

pid_t sid;  
sid = getsid(0);  
if(sid == -1)  
    perror("getsid");  
else  
    printf("My session id=%d\n",sid);  

2)与进程组相关的系统调用:

setpgid将进程pid的进程的进程组ID设置为pgid:

#include <unistd.h>  
  
int setpgid(pid_t pid, pid_t pgid);

如果pid是0,则使用调用者的进程id。如果pgid是0,则将pid进程的id设置为进程组的ID。
成功返回0。
a)pid代表的进程必须是调用者或者是其子进程,而且子进程没有调用过exec函数,并且pid进程和调用者在同一个会话中。
b)pid进程不能使会话首进程。
c)如果pgid已经存在,那么必须与调用者在同一个会话中。
d)pgid非负。

错误返回-1,并且把errno设置成:
EACCESS pid进程是迪欧用进程的子进程,并且子进程调用了exec函数。
EINVAL pgid < 0
EPERM pid进程是会话的首进程,或者与调用者不在同一个会话中的另一个进程。也可能试图把进程放置到一个不同在一个会话的进程组中。
ESRCH pid不是当前进程或者当前进程的子进程。

可以通过会话获得进程的进程组ID:

#include <unistd.h>  
  
pid_t getpgid(pid_t pid);  

成功返回pid进程组的ID。如果pid是0,返回当前进程的进程组ID,出错返回-1,而errno的唯一值是ERSCH,表示pid是一个非法的进程标识符。

获得当前进程的进程组ID:

pid_t pgid;  
pgid = getpgid(0);  
if(pgid == -1)  
    perror("getpgid");  
else  
    printf("My process group id=%d\n",pgid);  

参考:

  1. 《Linux system programming》
  2. 《Unix system programming》
  3. 《Advanced Programming in the Unix Environment》
  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值