进程基础

1. 进程标志

#include <unistd.h>
#include <sys/types.h>
uid_t getpid(void);
uid_t getppid(void);
uid_t getuid(void);
uid_t geteuid(void);
gid_t getgid(void);
git_t getegid(void);
struct passed *getpwuid(uid_t uid);
struct passwd { char *pw_name; /* 登录名称 */ char *pw_passwd; /* 登录口令 */ uid_t pw_uid; /* 用户 ID */ gid_t pw_gid; /* 用户组 ID */ char *pw_gecos; /* 用户的真名 */ char *pw_dir; /* 用户的目录 */ char *pw_shell; /* 用户的 SHELL */ }; #include <pwd.h>; #include <sys/types.h>;

2. fork

pid_t fork();

当 fork 掉用失败的时候(内存不足或者是用户的最大进程数已到)fork 返回-1.父进程中返回子进程id,子进程返回0。

fork()创建子进程就是父进程的一份拷贝,大部分属性都继承过来,但仍有部分属性不同。

1)子进程继承父进程属性

》真实用户ID和组ID,有效用户ID和组ID。

》进程组ID

》session ID

》所有打开文件及文件的偏移量。

》控制终端

》设置用户ID和设置组ID标记位

》根目录和当前工作目录

》文件默认创建的权限掩码

》可访问的内存区段

》环境变量及其他资源分配

2)子进程独有属性

》进程ID

》运行时间记录,timer等

》父进程对文件的锁定

》exec时子进程的信号处理函数指针组置为空,fork时子进程继承父进程信号处理函数(共用相同代码)

3. wait和waitpid

#include <sys/types.h>;

#include <sys/wait.h>;

pid_t wait(int *stat_loc);

pid_t waitpid(pid_t pid,int *stat_loc,int options);

两个系统调用用于等待子进程状态改变,并获取状态信息。如果该进程没有子进程或子进程已经结束,则wait/waitpid就会立即返回。

wait()阻塞执行直到子进程终止。

waitpid()默认情况下仅仅等待终止的子进程。但可通过options指定另外两种子进程状况改变。状态改变包含: the child terminated(默认,options=0); the child was stopped by a signal(options=WUNTRACED); or the child was resumed by a signal (options=WCONTINUED). 在子进程终止情况下,wait/waitpid准许系统释放子进程相关资源;若没有调用wait或waitpid子进程变为僵尸进程。

wait <==> waitpid(-1, &status, 0);

假如chilid已经改变状态,wait/waitpid会立即返回,否则两函数阻塞直到子进程状态改变或两函数被中断。若子进程状态改变,但还没有被wait/waitpid,则该进程被称为waitable。

waitpid()中pid取值范围如下:

< -1 等子进程中进程组ID等于pid绝对值的任一进程。

-1 等任一子进程。

0 等进程组ID等于调用进程组ID的任一进程。

> 0 等指定pid的状态改变。

waitpid()的options为0或(|)下列值:

WNOHANG,父进程不阻塞直接返回。

WUNTRACED,假如一个子进程停止(不能通过ptrace跟踪)就返回。若此选项不指定,则可追踪(traced)的已停止子进程的状态被返回。

WCONTINUED,一个已停止的进程由于收到信号SIGCONT而恢复则返回。

stat_loc保存子进程退出状态(一般exit,_exit,return),是一个整型指针,存储退出状态。若stat_loc不为NULL,则wait/waitpid储存退出状态(int型)在stat_loc中。可通过下列宏判断,参数为整型数,非指针。

WIFEXITED(status) return true假如子进程正常终止(exit,_exit, return).

》》WEXITSTATUS(status) 返回子进程的退出状态,其由status的最低8bits组成。仅当WIFEXITED返回真时有效。

WIFSIGNALED(status) 返回真,假如子进程被信号终止。

》》WTERMSIG(status) 返回信号数字值(造成子进程终止的信号),仅当WIFSIGNALED为真时有效。

》》WCOREDUMP(status) 返回真,假如子进程生成core dump。仅当WIFSIGNALED为真时有效。

WIFSTOPPED(status) 返回真,假如子进程被信号stopped。仅当options包含WUNTRACED或子进程正在被traced时有效。

》》WSTOPSIG(status) 返回信号数字值(造成子进程stop的信号)。仅当WIFSTOPPED为真时有效。

WIFCONTINUED(status) 返回真,假如子进程被SIGCONT恢复。

RETURN VALUE

wait(): on success, returns the process ID of the terminated child; on error, -1 is returned

waitpid(): on success, returns the process ID of the child whose state has changed; if WNOHANG was specified and one or more child(ren) specified by pid exist, but have not yet changed state, then 0 is returned.On error, -1 is returned.

4. SIGCHLD

SIGCHLD信号产生的条件

1.子进程终止时会向父进程发送SIGCHLD信号,告知父进程回收自己,但该信号的默认处理动作为忽略,因此父进程仍然不会去回收子进程,需要捕捉处理实现子进程的回收;

(注意:需要注意的是,虽然进程对于 `SIGCHLD`的默认动作是忽略,但是还是显示写出来,才能有效;signal(SIGCHLD, SIG_IGN),这样子进程直接会退出。)

2.子进程接收到SIGSTOP(19)信号停止时;

3.子进程处在停止态,接受到SIGCONT后唤醒时。

综上:子进程结束、接收到SIGSTOP停止(挂起)和接收到SIGCONT唤醒时都会向父进程发送SIGCHLD信号。父进程可以捕捉该信号,来实现对子进程的回收,或者了解子进程所处的状态。

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <signal.h>
 
void sys_err(char *str)
{
    perror(str);
    exit(1);
}
 
void do_sig_child(int signo)
{
    int status;
    pid_t pid;

// 不可以将捕捉函数内部的while替换为if。因为在执行捕捉函数期间,发送了多次SIGCHLD信号,未决信号集只是记录了一次
// 因此下一次再调用捕捉函数时,if只能完成对一个子进程的回收(即使有多个子进程都发了信号,但是只是调用一次捕捉函数)。
// 而while循环则可以对所有结束了的子进程都完成回收。因此对于多个子进程的回收,最好采用循环的方式,不采用if。
// if ((pid = waitpid(0, &status, WNOHANG)) > 0) { while ((pid = waitpid(0, &status, WNOHANG)) > 0) { if (WIFEXITED(status)) printf("------------child %d exit with %d\n", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) printf("child %d killed by the %dth signal\n", pid, WTERMSIG(status)); } } int main(void) { pid_t pid; int i; //阻塞SIGCHLD for (i = 0; i < 10; i++) { if ((pid = fork()) == 0) break; else if (pid < 0) sys_err("fork"); } if (pid == 0) { //10个子进程 int n = 1; while (n--) { printf("child ID %d\n", getpid()); sleep(1); } return i+1; //子进程结束状态依次为1、2、••••••、10 } else if (pid > 0) { struct sigaction act; act.sa_handler = do_sig_child; sigemptyset(&act.sa_mask); act.sa_flags = 0; sigaction(SIGCHLD, &act, NULL); //解除对SIGCHLD的阻塞 while (1) { printf("Parent ID %d\n", getpid()); sleep(1); } } return 0; }

父子进程信号处理:

1.子进程继承了父进程的信号屏蔽字和信号处理动作,但子进程没有继承未决信号集spending。

2.注意注册信号捕捉函数的位置。

3.应该在fork之前,阻塞SIGCHLD信号。注册完捕捉函数后解除阻塞。

参考:SIGCHLD信号 Linux: 关于 SIGCHLD 的更多细节

5.进程信号

访问共享资源时,进程与信号处理函数存在竞态,所以要互斥访问。同样也会造成死锁。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#include <signal.h>

pthread_mutex_t mmutex = PTHREAD_MUTEX_INITIALIZER;

void signal_handler(int signo)
{
    printf("signal...\n");

    pthread_mutex_lock(&mmutex);
    int i;
    for(i = 0; i < 5; i++){
        sleep(1);
        printf("signal run [%d]!\n", i);
    }
    pthread_mutex_unlock(&mmutex);
}


int main()
{
    int i;
    signal(SIGUSR1, signal_handler);

    pthread_mutex_lock(&mmutex);
    for(i = 0; i < 5; i++){
        sleep(1);
        printf("main run [%d]!\n", i);
        if(i == 2){
            raise(SIGUSR1);
        }
    }
    pthread_mutex_unlock(&mmutex);

    pthread_mutex_destroy(&mmutex);

    printf("main exit [%d]!\n", i);
    return 0;
}

信号处理函数编程注意

在用sigaction函数登记的信号处理函数中可以做的处理是被严格限定的,仅仅允许做下面的三种处理:

1. 局部变量的相关处理

2. “volatile sig_atomic_t”类型的全局变量的相关操作

3. 调用异步信号安全的相关函数

以外的其他处理不要做!若要使用其他请慎重考虑!

参考:准则2: 要知道信号处理函数中可以做那些处理

6. 僵尸进程

一个进程使用fork创建子进程,如果子进程退出,而父进程并没有调用 wait 或 waitpid 获取子进程的状态信息,那么子进程的进程描述符仍然保存在系统中。这种进程称之为僵尸进程。

僵尸进程危害

1)占用资源(少量,进程描述符task_struct存在,进程占用的资源被回收,不存在内存泄漏,实际上基本不浪费系统资源,参宋宝华的课程);

2)难以清除(杀死僵尸进程父进程)。

成为僵尸进程的因素

1. 子进程 先于 父进程退出;

2. 子进程的状态信息,没有被父进程回收;

那么问题来了,子进程退出了,父进程怎么知道呢?

对该机制有稍微了解的话,不难得知一个关键因素:SIGCHLD正是这个SIGCHLD起到了通知的作用,所以后面的处理也是基于它而实现。

僵尸进程处理方案

1. 父进程调用wait()或者waitpid()等待子进程结束,这样处理父进程一般会阻塞在wait处而不能处理其他事情。

2. 捕捉SIGCHLD信号,并在信号处理函数里面调用wait函数,这样处理可避免1中描述的问题。

3. 父进程直接忽略该信号。signal(SIGCHLD, SIG_IGN),这样子进程直接会退出。 需要注意的是,虽然进程对于 `SIGCHLD`的默认动作是忽略,但是还是显示写出来,才能有效(不显示写出来无效);

4. 把父进程杀了,子进程直接过继给 init,由 init伺候着。 不用担心 init会挂着一堆僵尸, init本身的设计就有专门回收的处理,所以有多少回收多少。实现:fork两次,父进程创建儿子进程,儿子进程再创建一个孙子进程,然后儿子进程自杀,孙子进程成为孤儿进程被init进程收养。

事后处理(杀父进程)

方法一: kill –18 PPID (PPID是其父进程)

这个信号是告诉父进程,该子进程已经死亡了,请收回分配给他的资源。

方法二:如果不行则看能否终止其父进程(如果其父进程不需要的话)。先看其父进程又无其他子进程,如果有,可能需要先kill其他子进程,也就是兄弟进程。方法是:

kill –15 PID1 PID2(PID1,PID2是僵尸进程的父进程的其它子进程)。

然后再kill父进程:kill –15 PPID

Z之所以杀不死,是因为它已经死了,否则怎么叫 Zombie(僵尸)呢?冤魂不散,自然是生前有结未解之故。

在UNIX/Linux中,每个进程都有一个父进程,进程号叫PID(Process ID),相应地,父进程号就叫PPID(Parent PID)。当进程死亡时,它会自动关闭已打开的文件,舍弃已占用的内存、交换空间等等系统资源,然后向其父进程返回一个退出状态值,报告死讯。如果程序有 bug,就会在这最后一步出问题。儿子说我死了,老子却没听见,没有及时收棺入殓,儿子便成了僵尸。在UNIX/Linux中消灭僵尸的手段比较残忍,执行 ps axjf 找出僵尸进程的父进程号(PPID,第一列),先杀其父,然后再由进程天子 init(其PID为1,PPID为0)来一起收拾父子僵尸,超度亡魂,往生极乐。

注意,子进程变成僵尸只是碍眼而已,并不碍事,如果僵尸的父进程当前有要务在身,则千万不可贸然杀之。

 

参考:

linux进程状态D和Z的处理

http://blog.csdn.net/eroswang/article/details/1774298

Linux: 关于 SIGCHLD 的更多细节

SIGCHLD信号

posted @ 2015-12-01 20:34  yuxi_o  阅读(647)  评论(0编辑  收藏  举报