Linux——进程控制

1.创建进程

创建进程的步骤:

  1. 给新建的进程分配一个进程标识符,在内核中创建PCB。
  2. 复制父进程的环境。
  3. 给子进程分配资源、栈、堆、代码、数据等。
  4. 复制父进程的地址空间内容到子进程的地址空间。
  5. 将进程置为就绪状态,放到就绪队列。

创建进程一般都是使用fork()函数,下面先简单了解一下这个函数。

头文件:
#include<unistd.h> /*#包含<unistd.h>*/
#include<sys/types.h> /*#包含<sys/types.h>*/

函数原型:
pid_t fork( void);

返回值:
  若成功调用一次则返回两个值,子进程返回0,父进程返回子进程ID;否则,出错返回-1。

fork()调用出错的原因:

  1. 系统中有太多进程。
  2. 实际用户的进程数超过了限制。

函数说明:

  一个现有进程可以调用fork函数创建一个新进程。由fork创建的新进程被称为子进程(child process)。fork函数被调用一次但返回两次。两次返回的唯一区别是子进程中返回0值而父进程中返回子进程ID。
  子进程是父进程的副本,它将获得父进程数据空间、堆、栈等资源的副本。注意,子进程持有的是上述存储空间的“副本”,这意味着父子进程间不共享这些存储空间。
  UNIX将复制父进程的地址空间内容给子进程,因此,子进程有了独立的地址空间。在不同的UNIX (Like)系统下,我们无法确定fork之后是子进程先运行还是父进程先运行,这依赖于系统的实现。所以在移植代码的时候我们不应该对此作出任何的假设。

关于fork()函数的使用:

  1. 创建出来的子进程和附近拥有各自的独立空间。
  2. 创建子进程后,父子进程交替运行,不存在谁先运行谁后运行。
  3. 如果父进程先死亡,子进程就成了孤儿进程,会被托管给1号进程,1号进程就是它新的进程。
  4. 如果子进程先死亡,在父进程回收它之前,它就是一个僵尸进程。此时可以杀死它的父进程来终止这个僵尸进程。
  5. 子进程刚被创建出来时,不会给它拷贝全部的空间,但当父进程或子进程对程序数据进行修改时,为防止影响另一个进程,此时就会给子进程拷贝。这种拷贝成为写时拷贝。

孤儿进程实例:

int main()
{
    pid_t pid = fork();
    int i = 0;
    if(pid == 0){
        for(i = 0; i < 10; ++i){
            printf("child process\n");
            sleep(1);
        }
        exit(0);
    }else if(pid > 0){
        for(i = 0; i < 5; ++i){
            printf("father process\n");
            sleep(1);
        }
        exit(0);
    }
}

查看结果:

[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
/*      进程id  父进程id    */
tian     32731 32584  0 00:22 pts/1    00:00:00 ./a.out 
tian     32732 32731  0 00:22 pts/1    00:00:00 ./a.out

[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
tian     32732     1  0 00:22 pts/1    00:00:00 ./a.out

[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
[tian@bogon ~]$ 

僵尸进程实例:

int main()
{
    pid_t pid = fork();
    int i = 0;
    if(pid == 0){
        for(i = 0; i < 5; ++i){
            printf("child process\n");
            sleep(1);
        }
        exit(0);
    }else if(pid > 0){
        for(i = 0; i < 10; ++i){
            printf("father process\n");
            sleep(1);
        }
        exit(0);
    }
}

查看结果:

[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
tian     32765 32584  0 00:28 pts/1    00:00:00 ./a.out
tian     32766 32765  0 00:28 pts/1    00:00:00 ./a.out

[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
tian     32765 32584  0 00:28 pts/1    00:00:00 ./a.out
tian     32766 32765  0 00:28 pts/1    00:00:00 [a.out] <defunct>

[tian@bogon ~]$ ps -ef | grep a.out | grep -v grep
[tian@bogon ~]$ 

关于fork()接下来说几道面试题:

面试题1:运行下面代码,会打印几次”hello word!” ?
答案是8次。

int main()
{
    fork();
    fork();
    fork();
    printf("hello word!\n");
    return 0;
}

面试题二:使用fork()函数给一个父进程创建两个子进程

int main()
{
    if(fork() > 0){
        fork();
    }
    printf("mypid=%d, parent=%d\n", getpid(), getppid());
    sleep(1);
    return 0;
}

查看结果:

mypid=373, parent=32712
mypid=375, parent=373
mypid=374, parent=373

面试题3:给一个父进程创建一个子进程,一个孙子进程

int main()
{
    if(fork() == 0){
        fork();
    }
    printf("mypid=%d, parent=%d\n", getpid(), getppid());
    sleep(1);
    return 0;
}

查看结果:

mypid=383, parent=32712
mypid=384, parent=383
mypid=385, parent=384

关于vfork()函数:

  1. vfork()用于创建一个子进程,但子进程会和父进程共享同一块地址空间空间,而fork()出来的子进程有独立地址空间。
  2. vfork()保证子进程先运行,在它调用exec或者exit后父进程才能被调度运行。

2.等待进程

为什么要进程等待?

  1. 前面我们提到如果子进程死亡,而父进程没有回收它,他就会成为僵尸进程,而僵尸进程会占用少量的系统资源,是有害的,并且很难被干掉。
  2. 父进程要通过进程等待的方式回收子进程的资源,获取子进程的退出信息。

使用wait()函数实现进程等待

头文件:
#include<sys/types.h>
#include<sys/wait.h>

函数原型:
pid_t wait (int * status);

返回值:
  如果执行成功则返回子进程识别码(PID),如果有错误发生则返回-1。失败原因存于errno 中。

参数:
  输出型参数,获取子进程退出状态,不关心可以设置成NULL。(输出型参数的意思是,你先创建一个变量,然后把它的地址传给wait,wait会把子进程的退出状态写入到这个变量中)

status 是一个int类型数据,它的第8到第16位记录的是退出码
查看方式如下:
printf(“%d\n”,((status >>8)&0xFF));
因为只有八个位表示退出码,且是无符号整型,所以退出码的范围是0~255

wait()函数使用示例:

int main()
{
    pid_t pid = fork();

    int i = 0;
    if(pid > 0)
    {
        for(i = 0; i < 3; ++i){
            printf("father: %d\n", getpid());
            sleep(1);
        }
        pid_t p;
        int status = 0;
        if((p = wait(&status)) == -1) //阻塞等待子进
            perror("wait");
        //输出等待的子进程id
        printf("father is wait:%d, child id:%d\n", p, pid);
    }

    if(pid == 0){
        for(i = 0; i < 6; ++i){
            printf("child : %d\n", getpid());
            sleep(1);
        }
    }

    return 0;
}

查看结果:

ps -ef | grep a.out | grep -v grep
father: 640
child : 641
father: 640
child : 641
father: 640
child : 641
child : 641
child : 641
child : 641
father is wait:641, child id:641

关于pidwait()函数

头文件:
#include <sys/types.h>
#include <sys/wait.h>

函数原型:
pid_t waitpid(pid_t pid, int * status, int options);

函数说明:
  waitpid()会暂时停止目前进程的执行, 直到有信号来到或子进程结束. 如果在调用waitpid()时子进程已经结束, 则waitpid()会立即返回子进程结束状态值. 子进程的结束状态值会由参数status 返回, 而子进程的进程识别码也会一块返回. 如果不在意结束状态值, 则参数status 可以设成NULL.。参数pid 为欲等待的子进程识别码, 其他数值意义如下:

  1. pid<-1 等待进程组识别码为pid 绝对值的任何子进程.
  2. pid=-1 等待任何子进程, 相当于wait().
  3. pid=0 等待进程组识别码与目前进程相同的任何子进程.
  4. pid>0 等待任何子进程识别码为pid 的子进程.

参数option 可以为0 或下面的OR 组合:

  1. WNOHANG:如果pid指定的子进程没有结束,则waitpid()返回0,不予等待,若正常结束,返回该子进程的id。
  2. WUNTRACED:如果子进程进入暂停执行情况则马上返回, 但结束状态不予以理会.。子进程的结束状态返回后存于status, 底下有几个宏可判别结束情况
  3. WIFEXITED(status):如果子进程正常结束则为非真。
  4. WEXITSTATUS(status):取得子进程exit()返回的结束代码, 一般会先用 WIFEXITED 来判断是否正常结束才能使用此宏。
  5. WIFSIGNALED(status):如果子进程是因为信号而结束则此宏值为真。
  6. WTERMSIG(status):取得子进程因信号而中止的信号代码, 一般会先用WIFSIGNALED 来判断后才使用此宏.。
  7. WIFSTOPPED(status):如果子进程处于暂停执行情况则此宏值为真. 一般只有使用WUNTRACED时才会有此情况。
  8. WSTOPSIG(status):取得引发子进程暂停的信号代码, 一般会先用WIFSTOPPED 来判断后才使用此宏。

返回值:
  如果执行成功则返回子进程识别码(PID), 如果有错误发生则返回-1. 失败原因存于errno 中.

3.终止进程

退出过程:

  1. 释放资源,例如内存,进程中打开的文件等。
  2. 记账信息,例如某个程序在系统上何时运行,运行所用资源等。
  3. 将进程设置成僵尸状态。
  4. 转存储调度,即将CPU让给别的进程使用。

进程终止的方法:
1. 正常退出:

1. main函数退出
2. exit
3. 执行退出处理函数
4. 刷新缓存
5. 调用_exit

2. 异常退出

1. Ctrl+c
2. about
3. kill

exit()_exit()区别
_exit()函数的作用最为简单:直接使进程停止运行,清除其使用的内存空间,并销毁其在内核中的各种数据结构;
exit() 函数则在这些基础上作了一些包装,在执行退出之前加了若干道工序,也是因为这个原因,有些人认为exit已经不能算是纯粹的系统调用。

exit()函数与_exit()函数最大的区别就在于exit()函数在调用exit系统调用之前要检查文件的打开情况,把文件缓冲区中的内容写回文件,就是”清理I/O缓冲”。

4.模拟实现shell

关于execvp()函数:

函数原型:
int execvp(const char file, char const argv []);

函数说明:
execvp()会从PATH 环境变量所指的目录中查找符合参数file 的文件名,找到后便执行该文件,然后将第二个参数argv传给该欲执行的文件。

返回值:
如果执行成功则函数不会返回,执行失败则直接返回-1,失败原因存于errno中。

execvp()函数实现过程:

  1. 删除mmu表。
  2. 内存开辟一块空间。
  3. 把可执行程序的代码复制到这块空间。
  4. 改变PCB中部分值,其中包括下一条要执行的指令的地址。
  5. 重新建mmu表。

注意:
execvp()会用即将运行的进程的内存替换为要调用的进程的内存,更进一步讲,就是把当前进程的机器指令都清空,然后载入被execvp运行起来的进程的机器指令。

模拟实现shell的思路:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(使用fork()函数)
  4. 替换子进程(使用execvp()函数)
  5. 父进程等待子进程退出(使用wait()函数)

< code >

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>
#include <ctype.h>

void do_exec(int argc, char* argv[])
{
    if( fork() == 0){  //判断是否为子进程
        execvp(argv[0], argv); //把正在执行的进程的内存替换成要执行的进程的内存
        perror("execvp");  //替换失败
        exit(1);
    }
    wait(NULL);  //阻塞等待子进程
}

void do_parse(char* buff)
{
    int i = 0;
    int status = 0;
    int argc = 0;
    char* argv[8] = {};

    for(i = 0; buff[i] != 0; ++i){ //根据空白符解析字符串
        if(status == 0 && !isspace(buff[i]))
        {
            status = 1;
            argv[argc++] = buff+i;
        }
        else if(isspace(buff[i])){
            status = 0;
            buff[i] = 0;
        }
    }
    argv[argc] = NULL;

    do_exec(argc, argv);
}

int main()
{
    char buff[1024];

    while(1){
        printf("my shell>");
        memset(buff, 0x00, sizeof(buff));

        while(scanf("%[^\n]%*c", buff) == 0){ //读取输入的字符串,
            while(getchar() != '\n');

            printf("my shell>");
        }
        //exit是系统内置命令,不可用execvp替换
        if(strncmp(buff, "exit", 4) == 0)
        {
            exit(0);
        }
        do_parse(buff);
    }
}

运行结果:

my shell>ls
a.out  myshell.c  test.c  testwait.c  wait.c
my shell>pe
execvp: No such file or directory  //不存在命令
my shell>pwd  
/home/tian/30/day5-进程销毁
my shell>exit                      //退出这个myshell进程
[tian@bogon day5-进程销毁]$ 

5. fork、execvp、popen和system的比较

1. fork创建子进程,以及对写时拷贝的解释

fork() 一个程序一调用fork函数,系统就为一个新的进程准备了前述三个段:

  1. 首先,系统让新的进程与旧的进程使用同一个代码段,因为它们的程序还是相同的,对于数据段和堆栈段,系统则复制一份给新的进程,这样,父进程的所有数据都可以留给子进程,但是,子进程一旦开始运行,虽然它继承了父进程的一切数据,但实际上数据却已经分开,相互之间不再有影响了,也就是说,它们之间不再共享任何数据了。而如果两个进程要共享什么数据的话,就要使用另一套函数(shmget,shmat,shmdt等)来操作。

  2. 现在,已经是两个进程了,对于父进程,fork函数返回了子程序的进程号,而对于子程序,fork函数则返回零,这样,对于程序,只要判断fork函数的返回值,就知道自己是处于父进程还是子进程中。

  3. 事实上,目前大多数的unix系统在实现上并没有作真正的copy。一般的,CPU都是以“页”为单位分配空间的,象INTEL的CPU,其一页在通常情况下是4K字节大小,而无论是数据段还是堆栈段都是由许多“页”构成的,fork函数复制这两个段,只是“逻辑”上的,并非“物理”上的,也就是说,实际执行fork时,物理空间上两个进程的数据段和堆栈段都还是共享着的,当有一个进程写了某个数据时,这时两个进程之间的数据才有了区 别,系统就将有区别的“页”从物理上也分开。系统在空间上的开销就可以达到最小。

2. exec修改子进程

  对于exec系列函数 一个进程一旦调用exec类函数,它本身就“死亡”了,系统把代码段替换成新的程序的代码,废弃原有的数据段和堆栈段,并为新程序分配新的数据段与堆栈段,唯一留下的,就是进程号,也就是说,对系统而言,还是同一个进程,不过已经是另一个程序了。

  不过exec类函数中有的还允许继承环境变量之类的信息,这个通过exec系列函数中的一部分函数的参数可以得到。

3. popen

  对于popen函数,他会通过command参数重新启动shell命令,并建立两个进程间的管道通信.
详见:popen函数_Linux C 中文函数手册

4. system

  对于system函数,它也会重新启动shell命令,当执行完毕后,程序会继续system下一行代码执行.
详见:system函数_Linux C 中文函数手册

注:以上内容摘自:popen system fork exec等函数的区别

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值