Linux进程控制

Linux进程简介

进程是操作系统中的一个重要概念,它是一个程序的一次执行过程,程序是进程的一种静态描述,系统中运行的每一个程序都是在它的进程中运行的。

进程4要素

  • 要有一段程序供该进程运行
  • 进程专用的系统堆栈空间
  • 进程控制块(PCB),具体实现是task_struct结构
  • 有独立的存储空间

Linux系统中所有的进程是相互联系的,除了初始化进程外,所有进程都有一个父进程。新的进程不是被创建,而是被复制,或是从以前的进程复制而来。Linux中所有的进程都是由一个进程号为1的init进程衍生而来的。

Linux系统包括3种不同类型的进程,每种进程都有自己的特点和属性:

  • 交互进程:由一个Shell启动的进程,既可以在前台运行,又可以在后台运行
  • 批处理进程:这种进程和终端没有联系,是一个进程序列
  • 监控进程(守护进程):Linux启动时启动的进程,并在后台运行

进程控制块

在Linux中,每个进程在创建时都会被分配一个数据结构,称为进程控制块(PCB, Process Control Block),描述进程的运动变化过程,与进程是一一对应的关系。通常PCB包含以下信息:

  • 进程标识符:每个进程的唯一标识符,可以是字符串,也可以是数字。
  • 进程当前状态:为方便管理,相同状态的进程会组成一个队列,如就绪进程队列。
  • 进程相应的程序和数据地址:以便把PCB与其程序和数据联系起来。
  • 进程资源清单:列出所有除CPU外的资源记录,如拥有的I/O设备,打开的文件列表等。
  • 进程优先级:反映进程的紧迫程度,通常由用户指定和系统设置。
  • CPU现场保护区:当进程因某种原因不能继续占用CPU时,释放CPU,需要将CPU的各种状态信息保护起来。
  • 进程同步与通信机制:用于实现进程间互斥、同步和通信所需的信号量等。
  • 进程所在队列PCB的链接字:根据进程所处的现行状态,进程相应的PCB参加到不同队列中,PCB链接字指出该进程所在队列中下一进程PCB的首地址。
  • 与进程有关的其它信息:如进程记账信息,进程占用CPU的时间等。

通过ps命令可以查看系统中目前有多少进程正常运行

通过ps-aux命令可以查看每个进程的详细信息

进程控制的相关函数

fork()函数

系统调用fork()函数派生一个进程,函数原型为:

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

运行成功,父进程返回子进程ID,子进程饭0;运行出错返回-1。

fork系统调用的作用是复制一个进程,从而出现两个几乎一样的进程。一般来说,fork后是父进程先执行还是子进程先执行是不确定的,取决于内核所实使用的调度算法。

fork函数示例,fork_test.c:

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int count = 0;
    pid_t pid;

    pid = fork();
    if(pid < 0)
    {
        printf("error in fork!");
        exit(1);
    }
    else if(pid == 0)
        printf("I am the child process, the count is %d, my process ID is%d\n",                                                        count, getpid());
    else
        printf("I am the parent process, the count is %d, my process ID is%d\n",                                                        ++count, getpid());

    return 0;
}

编译后运行:

$ ./fork_test
I am the parent process, the count is 1, my process ID is2308
I am the child process, the count is 0, my process ID is2309

在语句pid = fork();之前,只有一个进程在执行代码,但在该语句之后,有两个进程在执行之后的代码,根据pid的不同执行不同的语句。

fork调用的神奇之处在于被调用一次,能够返回两次,返回结果可能有3种情况:

  • 父进程中:fork返回新创建的子进程的ID
  • 子进程中:fork返回0
  • 出现错误:fork返回负值

fork出错的原因有2:

  • 当前进程数已达系统规定的上限,此时errno的值被设置为EAGAIN
  • 系统内存不足,此时errno的值被设置为ENOMEN

errno是Linux下的一个宏定义常量,当Linux中C API函数发生异常时,一般会将errno变量赋值为一个正整数(需include<errno.h>),不同的值表示不同的含义,通过查看该值可推测出错原因。

vfork()函数

vfork()与fork()的区别是:fork()需要复制父进程的数据段,而vfork()不需要完全复制,在子进程调用exec()或exit()之前,子进程与父进程共享数据段。fork()不对父子进程的执行次序作限制,而vfork()调用后,子进程先运行,父进程挂起,直到子进程调用了exec()或exit()后,父子进程的执行次序才不再有限制。

实际上,vfork()创建出的不是真正意义的进程,它缺少了进程4要素的最后一项——独立的内存资源。

vfork()创建父子进程共享数据段测试,vfork_test1.c():

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int count = 1;
    int child;
    printf("Before create son, the father's count is:%d\n", count);
    child = vfork();
    if(child < 0)
    {
        printf("error in vfork!");
        exit(1);
    }
    if(child == 0)
    {
        printf("This is son, his pid is:%d and the count is:%d\n", getpid(),++ count);
        exit(1);
    }
    else
        printf("After son, This is father, his pid is:%d and the count is:%d, and the child is:%d\n", getpid(), count, child);

    return 0;
}

编译后运行:

$ ./vfork_test1
Before create son, the father's count is:1
This is son, his pid is:2530 and the count is:2
After son, This is father, his pid is:2529 and the count is:2, and the child is:2530

可以看出,在子进程中修改了count的值,变为2,而父进程中count值也为2,说明父子进程共享count,即父子进程共享内存区。

vfork()创建子进程导致父进程挂起测试,vfork_test2():

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int count = 1;
    int child;
    printf("Before create son, the father's count is:%d\n", count);
    if(!(child = vfork()))
    {
        int i;
        for(i=0; i<100; i++)
        {
            printf("This is son, the i is:%d\n", i);
            if(i == 70)
                exit(1);
        }
        printf("This is son, his pid is:%d and the count is:%d\n", getpid(), ++count);
        exit(1);
    }
    else
        printf("After son, This is father, his pid is:%d and the count is:%d, and the child is:%d\n", getpid(), count, child);

    return 0;
}

编译后运行:

$ ./vfork_test2
Before create son, the father's count is:1
This is son, the i is:0
This is son, the i is:1
This is son, the i is:2
...省略
This is son, the i is:67
This is son, the i is:68
This is son, the i is:69
This is son, the i is:70
After son, This is father, his pid is:2541 and the count is:1, and the child is:2542

可以看出,父进程是等待子进程执行完毕后才开始执行。

exec函数族

Linux使用exec函数族来执行新的程序,以新的子进程来完全代替原有的进程,exec函数族包含6个函数:

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

运行成功无返回,出错返回-1。

  • 函数中含义字母l的:其参数个数不定,参数由命令行参数列表组成,最v后一个NULL表示结束。
  • 函数中含义字母v的:使用一个字符串数组指针argv指向参数列表,与含字母l的函数参数列表完全相同。
  • 函数中含义字母p的:可以自动在环境变量PATH指定的路径中搜索要执行的程序,其第一参数filename为可执行函数的文件名,注意其它函数的第一个参数pathname为路径名
  • 函数中含义字母e的:比其它函数多了一个字符串指针型的envp参数,用于指定环境变量。

实际上,只有execve()函数才是真正意义上的系统调用,其它都是在此基础上经过包装的库函数。与一般情况不同,exec函数族执行成功后不会返回,因为调用进程实体,包括代码段、数据段和堆栈段都被新的内容取代,只是进程ID等一些表面上的信息仍保持原样。

exec函数族使用举例,exec_example.c:

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

int main(void)
{
    char *envp[] = {"PATH=/tmp", "USER=root", "STATUS=testing", NULL};
    char *argv_execv[] = {"echo", "excuted by execv", NULL};
    char *argv_execvp[] = {"echo", "excuted by execvp", NULL};
    char *argv_execve[] = {"env", NULL};

    if(fork()==0)
    {
        if(execl("/bin/echo", "echo", "executed by execl", NULL))
            perror("Err on execl");
    }
    if(fork()==0)
    {
        if(execlp("echo", "echo", "executed by execlp", NULL))
            perror("Err on execlp");
    }
    if(fork()==0)
    {
        if(execle("/usr/bin/env", "env", NULL, envp))
            perror("Err on execle");
    }
    if(fork()==0)
    {
        if(execv("/bin/echo", argv_execv))
            perror("Err on execv");
    }
    if(fork()==0)
    {
        if(execvp("echo", argv_execvp))
            perror("Err on execvp");
    }
    if(fork()==0)
    {
        if(execve("/usr/bin/env", argv_execve, envp))
            perror("Err on execve");
    }
    return 0;
}

上述程序用到了perror()函数,它用来将函数发生错误的原因输出到标准输出(stderr),其函数原型为:

#include <stdio.h>
void perror(const char *s)

编译后执行:

$ ./exec_example
PATH=/tmp
USER=root
STATUS=testing
executed by execl
executed by execlp
$ PATH=/tmp
USER=root
STATUS=testing
excuted by execvp
excuted by execv

由于各子进程执行的顺序无法控制,因而每次运行结果的输出顺序会有不同。

使用exec函数族,一般要加上错误判断语句,因为exec函数易由多种原因运行失败:

  • 找不到文件或路径:errno被设置为ENOENT
  • 数组argv和envp忘记使用NULL结束:errno被设置为EFAULT
  • 没有文件的运行权限:errno被设置为EACCES

exit()与_exit()函数

这两个函数都是用于终止进程,其定义分别为:

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

两者主要区别在于:

  • 定义及所需头文件不同
  • _exit()立即进入内核;exit()则先执行一些清除处理(包括调用执行个终止处理程序,关闭所有标准I/O流等),然后进入内核。
  • exit()在调用之前要检查文件的打开情况,把文件缓冲区的内容写回文件;_exit()则直接使进程停止,清除其使用的内存空间,并销毁其在内核中的各种数据结构。

在Linux的标准函数库中,有一套被称为“高级I/O的函数”,如printf()、fopen()等,也被称为“缓冲I/O(buffered I/O)”,其特征是对应每一个打开的文件,在内存中都有一片缓冲区,每次会多读出若干条记录,当达到一定的条件(如达到一定数量,或遇到特定字符,如’\n’和文件结束符EOF)时,再将缓冲区的内容一次性写入文件,从而增加读写速度。但是,这种情况下,如果使用_exit()退出,会导致某些数据未被保存,而用exit()则不会有问题。

exit()与_exit()函数的区别测试,exit_differ.c:

#include <sys/types.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    pid_t pid;
    if((pid=fork()) == -1)
    {
        printf("failed to create a new process\n");
        exit(0);
    }
    else if(pid == 0)
    {
        printf("\nchild process, output begin\n");
        printf("child process, content in buffer");
        _exit(0);
    }
    else
    {
        printf("parent process, output begin\n");
        printf("parent process, content in buffer");
        exit(0);
    }

    return 0;
}

编译后执行:

$ ./exit_differ
parent process, output begin
parent process, content in buffer
child process, output begin

由于printf函数遇到’\n‘时才从缓冲区读取数据,在子进程中,因为_exit(0)直接将缓冲区的内容清除了,内容没有显示;而父进程中,执行exit(0)之前会先将缓冲区的内容显示出来。

wait()与waitpid()函数

在一个进程调用了exit()之后,该进程并非立即消失,而是留下一个僵尸进程(Zombie)的数据结构,这时的一种处理方法就是使用wait()waitpid()函数。

僵尸态是进程的一种特殊状态,没有任何可执行代码,也不能被调度,仅仅在进程中保留一个位置,记载改进程的退出状态等信息供其它进程收集。

wait()waitpid()函数原型:

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

运行成功返回进程ID,出错返回-1。

参数status用于保存进程退出时的一些状态,如果只是想把进程灭掉,可以设置该参数为NULL。

参数pid用于指定所等待的线程。

pid取值 含义
pid > 0 只等待进程ID为pid的子线程
pid = -1 等待任何一个子线程,此时waitpid等价于wait
pid = 0 等待同一个进程组中的任何子进程
pid < -1 等待一个指定进程组中的任何子进程,其进程ID为pid的绝对值

参数options提供一些额外的选项来控制waitpid,包括WNOHANGWUNTRACED两个选项,这是两个常数,可以用|运算符连接使用。其中WNOHANG参数用于设置不等待子进程退出,立即返回,此时waitpid返回0;WUNTRACED参数用于配置跟踪调试。

进程一旦调用wait后,就立刻阻塞自己,如果当前进程的某个子进程已退出,则收集其信息,否则wait会一种阻塞在这里,直到有一个僵死进程出现。

wait()示例

wait_example.c:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    pid_t pc, pr;
    if((pc = fork()) < 0)
    {
        printf("error in fork!");
        exit(1);
    }
    else if(pc == 0)
    {
        printf("This is child process with pid of %d\n", getpid());
        sleep(10);
    }
    else
    {
        pr = wait(NULL);
        printf("I catched a child process with pid of %d\n", pr);
    }

    exit(0);
}

编译后执行:

$ ./wait_example
This is child process with pid of 10093
I catched a child process with pid of 10093

可以看到,第1行输出后,等待大约10秒,第2行才输出,这10秒就是子线程的睡眠时间。

waitpid()示例

父进程和子进程分别睡眠10秒钟和1秒钟,代表所作的相应工作。父进程利用工作的简短间歇查看子进程是否退出,如果退出就收集它。waitpid_example.c:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    pid_t pc, pr;
    if((pc = fork()) == -1)
    {
        printf("failed to create a new process");
        exit(0);
    }
    else if(pc == 0)
    {
        sleep(10);
        exit(0);
    }

    do
    {
        pr = waitpid(pc, NULL, WNOHANG);
        if(pr == 0)
        {
            printf("No chiled exited\n");
            sleep(1);
        }
    }while(pr == 0);

    if(pr == pc)
        printf("successfully get child %d\n", pr);
    else
        printf("some error occured\n");

    return 0;
}

sdfgh

$ ./waitpid_example
No chiled exited
No chiled exited
No chiled exited
No chiled exited
No chiled exited
No chiled exited
No chiled exited
No chiled exited
No chiled exited
No chiled exited
successfully get child 2711

可以看到,父进程经过10次失败尝试后,终于收集到了退出的子进程。

获取子进程返回状态

对于wait()waitpid()中的status参数,当其值不为NULL时,子进程的退出状态会以int值的形式保存其中,通过一套专门的宏(macro)可以读取存入的状态值,这里只列举两个常用的宏:

宏定义 含义
WIFEXITED(status) 子进程正常退出时,返回一个非零值,否则返回零
WEXITSTATUS(status) 当WIFEXITED为真时,此宏才可用,返回该进程退出的代码

示例,子进程调用exit(3)退出,WIFEXITED(status)指示子进程正常退出,WEXITSTATUS(status)就会返回3。get_status.c:

#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int main(void)
{
    int status;
    pid_t pc, pr;
    if((pc = fork()) < 0)
    {
        printf("error in fork!");
        exit(1);
    }
    else if(pc == 0)
    {
        printf("This is child process with pid of %d.\n", getpid());
        exit(3);
    }
    else
    {
        pr = wait(&status);
        if(WIFEXITED(status))
        {
            printf("the child process %d exit normally.\n", pr);
            printf("the return code is %d.\n", WEXITSTATUS(status));
        }
        else
            printf("the child process %d exit abnormally.\n", pr);
    }

    return 0;
}

assvf

$ ./get_status
This is child process with pid of 2718.
the child process 2718 exit normally.
the return code is 3.

可以看出,父进程捕捉到了子进程的返回值3。

system()函数

函数原型:

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

sysytem()调用fork()产生子进程,由子进程来调用/bin/sh-cmdstring来执行参数cmdstring字符串所代表的命令,此命令执行完后随即返回原调用的进程。

编程示例,4次调用system,设置不同的命令行参数,system返回不同的结果,cmd_system.c:

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int status;
    if((status = system(NULL)) < 0)
    {
        printf("system error!\n");
        exit(0);
    }
    printf("exit status=%d\n", status);

    if((status = system("date")) < 0)
    {
        printf("system error!\n");
        exit(0);
    }
    printf("exit status=%d\n", status);

    if((status = system("invalidcommand")) < 0)
    {
        printf("system error!\n");
        exit(0);
    }
    printf("exit status=%d\n", status);

    if((status = system("who; exit 44")) < 0)
    {
        printf("system error!\n");
        exit(0);
    }
    printf("exit status=%d\n", status);

    return 0;
}

adss

$ ./cmd_system
exit status=1
2019年 12月 10日 星期二 14:55:36 CST
exit status=0
sh: 1: invalidcommand: not found
exit status=32512
deeplearning pts/0        2019-12-10 13:46 (192.168.1.110)
exit status=11264

第1次调用system,参数为NULL,返回结果为1,说明在本Linux系统下system可用;第2次调用system,参数为data,system成功执行;第3次调用system,参数为一个非法的字符串命令,返回结果shell的终止状态(命令出错)32512;第4次调用system,参数为who,显示登录用户情况,exit 44是退出当前的shell,system成功返回,返回值11264。

参考:《精通Linux C编程》- 程国钢

 

上一篇:org.apache.ibatis.reflection.ReflectionException: There is no getter for property named 'leader


下一篇:MOOC《Linux操作系统编程》学习笔记-实验五