进程是操作系统调度的最小的单位

进程的控制函数

进程的控制函数大概可以这样分类

  • 创建
    • fork
    • exec系
  • 结束
    • 正常,
    • 意外,出现意外就需要对意外进行控制处理。
  • 控制
    • 保存环境。内存, 信号量等环境处理,响应。
  • 信息
    • 比如什么getpid之类的
  • 结束后的处理。

创建

创建进程提供了很多函数

最多的常用的EXEC 系列函数

不过EXEC 并不是真正的创建一个进程,exec会将 系统给当前进程分配的内存空间内存(用户区,比如什么.data段啦,.bss段啦之类的。替代码区等待),进行一个重新赋值替换,然而PCB 进程控制块区域不变(在系统内核区部分)

换核不换壳,该进程的pid ppid都不变

真正创建进程的这是fork函数。

EXEC系列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <unistd.h>

extern char **environ;

int execl(const char *pathname, const char *arg, ...
/* (char *) NULL */);
int execlp(const char *file, const char *arg, ...
/* (char *) NULL */);
int execle(const char *pathname, const char *arg, ...
/*, (char *) NULL, char *const envp[] */);
int execv(const char *pathname, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[],
char *const envp[]);

execl

  • const char * pathname

    • 执行文件的路径,不过只可以输入绝对路径。
  • const char *arg
    • 第一个参数 默认填写程序自身。 第二个参数如果没有填NULL,因为默认NULL 结束

execlp

  • const char* file
    • 执行文件的路径,可以绝对也可以相对,相对位置为当前程序所在位置. 函数名上的p也可以体现
  • const char*arg
    • 同上没有变化,命名l的原因,va_list 的L
    • 执行参数 ,如果没有则为NULL;

execle

  • const char * pathname
    • 执行文件的绝对路径
  • const char* arg
    • 同上,以(char *)null结束
  • char * const envp[]
    • 环境变量(输入参数),可以自行设定执行文件程序的所携带的环境变量。

execv

  • const char * pathname
    • 执行文件的路径,不过只可以输入绝对路径。
  • char const *argv[]
    • 输入参数,类型是一个argv数组的地址
    • 参数的数组形式,第一个值也必须是自身。数组最后一个值也必须是NULL!

execvp

  • const char* file
    • 执行文件的路径,可以绝对也可以相对,相对位置为当前程序所在位置. 函数名上的p也可以体现
  • char const *argv[]
    • 输入参数,类型是一个argv数组的地址
    • 参数的数组形式,数组最后一个值也必须是NULL!

execve(内核级别)

  • const char * filename
    • 执行文件的绝对路径,不同的是也可以相对
  • char const *argv[]
    • 输入参数,类型是一个argv数组的地址
    • 参数的数组形式,数组最后一个值也必须是NULL!
  • char * const envp[]
    • 环境变量(输入参数),可以自行设定执行文件程序的所携带的环境变量。

当然还有个execvpe

FORK

一个简单的函数使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
#include <error.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int main() {
pid_t sonpid, fatherpid;
int log;

fatherpid = getpid();
sonpid = fork();
if (sonpid < 0) {
perror("fork error");
exit(1);
} else if (sonpid == 0) {
printf("is son %d !is ppid %d!\n", getpid(), getppid());
sleep(10);

} else {
printf("is parent %d, is ppid %d!\n", getpid(), getppid());
if (sonpid == wait(&log)) {
printf("log : %d\n", log);
return 0;
}
}

return 0;
}

我们可以从代码中得出几个信息

  • 进程与子进程,变量并不共通。
  • 进程的子进程结束后需要收尸,否则会产生僵尸进程,因为死了但是没管他,父进程一直在自己运行。
  • 当子进程出现异常,信号使他关闭后,父进程正常wait,得到的log值是信号编号。比如说kill -9 子进程 返回log的值为9

结束

正常退出与异常退出,大部分几乎程序的退出结束,在Linux中是不同的各种信号所影响的。

正常

abort

1
void abort(void);

手动报错,cored umped错误,将CPU 的寄存器的值dump,发送SIGABRT signal使进程结束。

assert

1
void assert(scalar expression);

需要调用头文件assert.h

If expression is false (i.e., compares equal to zero), assert() prints an error message to standard error and terminates the program by calling abort(3). The error message includes the name of the file and function containing the assert() call, the source code line number of the call, and the text of the argument;

手动断言,当scalar expression 中为 0 时, 将输入在stderror中一个断言执行的文件与代码语句所处位置。

atexit

1
int atexit(void (*function)(void));

进程结束后调用,需要填写一个带void 参数的函数指针,调用该函数。

on_exit

1
int on_exit(void (*function)(int , void *), void *arg);

子进程结束后调用,填写一个带int参数和void 参数的函数指针 ,调用该函数

_exit

1
2
void _exit(int status);

   _exit() terminates the calling process "immediately".  Any open file descriptors
   belonging to the process are closed.  Any children of the process are  inherited
   by  init(1) (or by the nearest "subreaper" process as defined through the use of
   the prctl(2) PR_SET_CHILD_SUBREAPER operation).  The process's parent is sent  a
   SIGCHLD signal.

   The  value status & 0xFF is returned to the parent process as the process's exit
   status, and can be collected by the parent using one of the  wait(2)  family  of
   calls.

   The function _Exit() is equivalent to _exit().

进程和描述符立刻关闭。并且发送一个SIGCHLD 信号关闭所有自己的子进程,然后向自己的爷爷进程告知自己的状态,这个状态 就是 status.的值&0XFF(限制字节大小范围),无论子进程结束后还是自己结束后有什么on_exit 或者,atexit 通通无效,主打的就是一个immediately

异常

异常,说白了也是信号导致的,不过异常的行为是人,也就是设计者的主观认为,是预期之外的错误。

出现异常关闭可以使用wait/waitpid收集到关闭的状态,加以分析。当然这并不是什么好办法。

c还提供了如下函数来对意外进行一个容错。

setjmp

1
2
3
4
5
#include <setjmp.h>

int setjmp(jmp_buf env);
int sigsetjmp(sigjmp_buf env, int savesigs);

作用是保存当前堆栈环境,当程序出现异常错误时,想要恢复(比如说服务器程序,通常利用该函数进行异常处理, 避免在其他函数出现崩溃后,直接就寄了,实现了一个异常捕获。

实际上C 语言中,这就是一个处理异常的方式 类似c++ 的 try catch error,

   The setjmp() function saves various information about the calling envi‐
   ronment (typically, the stack pointer, the instruction pointer,  possi‐
   bly  the  values  of other registers and the signal mask) in the buffer
   env for later use by longjmp().  In this case, setjmp() returns 0.

   The longjmp() function uses the information saved in  env  to  transfer
   control  back  to  the  point  where setjmp() was called and to restore
   ("rewind") the stack to its state at the time of the setjmp() call.  In
   addition,  and  depending on the implementation (see NOTES), the values
   of some other registers and the process signal mask may be restored  to
   their state at the time of the setjmp() call.

the values of some other registers and the process signal mask may be restored to their state at the time of the setjmp() call.

需要时刻注意的是,恢复之后,正常执行的运算保存在寄存器中的值,和信号遮罩可能会重置回到第一次执行setjmp的状态。

简单代码示例:一个简单的异常捕获的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
void test03() {
longjmp(jmpBuf, 2);
}
void test02() {
longjmp(jmpBuf, 1);
}

void test01() {
test02();
}

void signal_deal(int sig) {
if (sig == SIGSEGV) {
longjmp(jmpBuf, SIGSEGV);
}
}

int main() {
sighandler_t hsig = signal_deal;
// 注册一个段错误信号回调函数。
signal(SIGSEGV, hsig);

int jmpret = setjmp(jmpBuf);
if (jmpret == 0) {

// test01();
// 手动段错误。
*(int*)(NULL) = 0;

} else if (jmpret == 1) {
printf("处理异常 test02\n");
} else if (jmpret == 2) {
printf("处理异常 test03\n");
} else if (jmpret == SIGSEGV) {
printf("处理异常 段错误信号\n");
}

printf("主函数正常运行\n");
return 0;
}

sigjmpset

1
2
void longjmp(jmp_buf env, int val);
void siglongjmp(sigjmp_buf env, int val);

作用呢同setjmp,但是呢,保存的值保存的状态更全,不仅仅是寄存器,可以是上下文:堆栈,寄存器,状态新城进程,栈每次地址。

sigsetjmp()  and  siglongjmp() also perform nonlocal gotos, but provide predictable handling of the process signal mask.
 If, and only if, the savesigs argument provided to sigsetjmp() is  non‐
       zero, the process's current signal mask is saved in env and will be re‐
       stored if a siglongjmp() is later performed with this env.

根据文档,可见,sigsetjmp不进可以实现非本地的进程跳转,(线程,进程之间也可以跳转,改变进程之间顺序), 而且提供 可预测的,要调用(跳转的)进程信号遮罩 mask 信号屏蔽量。

常在逆向中使用。嘿嘿嘿嘿。

信号捕获衍生

sigaction

1
2
3
4
5
#include <signal.h>

int sigaction(int signum, const struct sigaction *act,
struct sigaction *oldact);

  • int signum

    • 信号码
  • const struct sigaction *act

    • 存放信号行为信息的结构体

      • ```c
        struct sigaction {
             void     (*sa_handler)(int);
             void     (*sa_sigaction)(int, siginfo_t *, void *);
             sigset_t   sa_mask;
             int        sa_flags;
             void     (*sa_restorer)(void);
         };
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        22
        23
        24
        25

        - 第一个 表示行为的调用规则

        常见的有 SIG_DFL 默认行为,SIG_IGN :不理睬捕获这个信号。

        - 第二个 这个信号的的回调函数的 函数指针。

        - 当前信号遮罩,用于信号集的过滤等。。

        - sa_flags 行为标志,根据不同的信号不同的回收之类的操作。

        - The sa_restorer field is not intended for application use. (POSIX does
        not specify a sa_restorer field.) Some further details of the purpose
        of this field can be found in sigreturn(2).

        # 信息

        ### 进程信息常用的获取函数

        - ```c
        #include <sys/types.h>
        #include <unistd.h>

        pid_t getpid(void);
        pid_t getppid(void);

得到进程的pid 和得到进程的ppid 也就是parent pid 父进程的pid

  • ```c
    pid_t getpgrp(void); / POSIX.1 version /
    pid_t getpgrp(pid_t pid); / BSD version /
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10

    不同系统内核下不同版本,得到当前的进程组id



    既然有get,那当然就有set.

    - ```c
    int setpgrp(void); /* System V version */
    int setpgrp(pid_t pid, pid_t pgid); /* BSD version */
All  of  these  interfaces  are available on Linux, and are used for getting and
setting the process group ID (PGID) of a process.  The preferred, POSIX.1-speci‐
fied ways of doing this are: getpgrp(void), for retrieving the calling process's
PGID; and setpgid(), for setting a process's PGID.

setpgid() sets the PGID of the process specified by pid  to  pgid.   If  pid  is
zero, then the process ID of the calling process is used.  If pgid is zero, then
the PGID of the process specified by pid is made the same as its process ID.  If
setpgid()  is  used  to  move a process from one process group to another (as is
done by some shells when creating pipelines), both process groups must  be  part
of  the same session (see setsid(2) and credentials(7)).  In this case, the pgid
specifies an existing process group to be joined and  the  session  ID  of  that
group must match the session ID of the joining process.


  The  System V-style  setpgrp(), which takes no arguments, is equivalent
   to setpgid(0, 0).

   The BSD-specific setpgrp() call, which takes arguments pid and pgid, is
   a wrapper function that calls

       setpgid(pid, pgid)

   Since  glibc 2.19, the BSD-specific setpgrp() function is no longer ex‐
   posed by <unistd.h>; calls should be replaced with the  setpgid()  call
   shown above.

​ 被取代为setpgid()咯。

而setpgid调用后,就会将该进程脱离父进程的组,

1
setpgid(pid, pid);//让子进程脱离父进程

将进程的组ID 设计为自己的ID 作为自己的组ID ,实际上。

得到进程的执行优先级

1
2
3
4
5
#include <sys/time.h>
#include <sys/resource.h>

int getpriority(int which, id_t who);
int setpriority(int which, id_t who, int prio);

The scheduling priority of the process, process group, or user, as in‐
dicated by which and who is obtained with the getpriority() call and
set with the setpriority() call. The process attribute dealt with by
these system calls is the same attribute (also known as the “nice”
value) that is dealt with by nice(2).

The value which is one of PRIO_PROCESS, PRIO_PGRP, or PRIO_USER, and
who is interpreted relative to which (a process identifier for
PRIO_PROCESS, process group identifier for PRIO_PGRP, and a user ID for
PRIO_USER). A zero value for who denotes (respectively) the calling
process, the process group of the calling process, or the real user ID
of the calling process.

  • int which
    • 三种不同宏,
    • PRIO_PROCESS 得到进程的优先级
    • PRIO_PGRP 得到进程组的优先级
    • PRIO_USER 得到用户的优先级
  • id_t who
    • 当然是id了,
  • set 进程组的优先级

The prio argument is a value in the range -20 to 19 (but see NOTES be‐
low). with -20 being the highest priority and 19 being the lowest pri‐
ority. Attempts to set a priority outside this range are silently
clamped to the range. The default priority is 0; lower values give a
process a higher scheduling priority.

prio通常的优先级范围从 -20 ~19 。数字越小进程优先越高。

我们常见的sleep函数呢,为了保证能够挂起指定的时间通常会将进程的优先级设为最高后设定挂起时间。

当我们进行一个进程的调试时,发现,进程的父进程就是gdb ,而gdb进程的子进程就是该被调试的进程。

可以得知:调试也是一个进程。

在修改优先级的时候,降级没问题, 而提升则是需要检查有效用户的权限。

结束后的处理

子进程结束后的处理。一般进程结束,比如,收尸啥的。

以下是常用得函数

1
2
3
4
5
6
7
8
9
10
#include <sys/types.h>
#include <sys/wait.h>

pid_t wait(int *wstatus);

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

int waitid(idtype_t idtype, id_t id, siginfo_t *infop, int options);
/* This is the glibc and POSIX interface; see
NOTES for information on the raw system call. */

wait

等待子进程中断或结束。防止出现僵尸进程。zone

不过这种wait只能wait一个子进程pid,无法wait多个进程,不可控的。

并且还不能设定wait的处理模式,只能阻塞等待

waitpid

waitpid的三个参数

  • pid_t pid
    • 需要监听的pid,不同的pid值有不同的意义
    • 返回小于-1 的值 表示 等待的所有子进程的进程组ID 比如-12323 等待组ID 为12323的 所有子进程
    • 等于 -1 表示 等待所有子进程
    • 0 表示等待任意调用进程组ID相同的组内的子进程。
  • int *wstatus
    • 传出的wait statu 状态,提供了几个宏来判断状态类型。
    • ​ WIFEXITED(wstatus) 返回 是否 普通的结束
    • ​ WEXITSTATUS(wstatus) 返会子进程的退出的状态码
    • ​ WIFSIGNALED(wstatus) 返回进程 是否 是由于信号导致的结束
    • ​ WTERMSIG(wstatus) 返回信号导致的结束的信号
    • ​ WCOREDUMP(wstatus) 返回 是否 是CORE DUMP 而结束进程
    • ​ WIFSTOPPED(wstatus) 返回是否是比一个a信号传递的导致的结束 与WUNTRACED 配合操作
    • ​ WSTOPSIG(wstatus) 返回信号,造成子进程信号值,搭配WIFSOPPED 使用
      • returns the number of the signal which caused the child to stop. This
        macro should be employed only if WIFSTOPPED returned true.
        
    • ​ WIFCONTINUED(wstatus) 返回是否是因为SIGCONT. 信号导致进程挂起。 也常常与与WCONTINUED 配合操作
  • int options
    • wait操作
      • WNOHANG 非阻塞
        • return immediately if no child has exited.
      • WUNTEACED 被调试
        • also return if a child has stopped (but not traced via ptrace(2)). Status for traced children which have stopped is provided even if this option is not specified.
      • WCONTINUED 发生了信号导致进程暂停后,
        • also return if a stopped child has been resumed by delivery of SIGCONT
  • return int
    • 如果回收成功一个进程后返回 该进程id
    • if WNOHANG was specified and one or more child(ren) specified by pid
      exist, but have not yet changed state, then 0 is returned
      
    • 当waitpid 为非阻塞回收时, 检测到还没有结束的子进程,返回0 表示没有回收,等待回收。
    • 失败返回-1 并输出error;