菜鸟学习历程【17】进程控制编程

进程控制编程

进程:进程是一个具有一定独立功能的程序的一次运行活动,同时也是资源分配的最小单元;

进程 程序
程序执行的实例 放到磁盘的可执行文件
进程不可在计算机之间迁移 程序通常对应着文件、静态和可以复制
动态 静态
暂时:进程是一个状态变化的过程 长久:程序可长久保存

进程与程序组成不同:进程的组成包括程序、数据和进程控制块(即进程状态信息)

进程与程序的对应关系:通过多次执行,一个程序可对应多个进程;通过调用关系,一个进程可包括多个程序。

进程的生命周期

创建: 每个进程都是由其父进程创建,进程可以创建子进程,子进程又可以创建子进程的子进程

运行: 多个进程可以同时存在,进程间可以通信

撤销: 进程可以被撤销,从而结束一个进程的运行

进程的状态(运行):

执行状态:进程正在占用CPU

就绪状态:进程已具备一切条件,正在等待分配CPU的处理时间片

等待状态:进程不能使用CPU,若等待事件发生则可将其唤醒

Linux进程

Linux系统是一个多进程的系统,它的进程之间具有并行性、互不干扰等特点。

也就是说,每个进程都是一个独立的运行单位,拥有各自的权利和责任。其中,各个进程都运行在独立的虚拟地址空间,因此,即使一个进程发生异常,它也不会影响到系统中的其他进程。

进程ID:(PID)标准进程的唯一数字
父进程:(PPID)
启动进程的用户ID(UID)

进程互斥:进程互斥是指当有若干进程都要使用某一共享资源时,任何时刻最多允许一个进程使用,其他要使用该资源的进程必须等待,直到占用该资源者释放了该资源为止。
临街资源:(共享资源)一次只允许一个进程访问的资源称为临界资源
临界区:进程中访问临界资源的那段程序代码称为临界区
进程同步: 一组并发进程按一定的顺序执行的过程称为进程间的同步,具有同步关系一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。
进程调度:按一定算法,从一组待运行的进程中选出一个来占有CPU运行。
调度方式:抢占(优先级) 、非抢占式

算法:

  • 先来先服务调度算法
  • 短进程优先调度算法
  • 高优先级优先调度算法
  • 时间片轮转法

目前,后两者最为常用。

死锁:多个进程因竞争资源而形成一种僵局,若无外力作用,这些进程都将永远不能再向前推进


获取ID

pid_t getpid(void) 获取进程ID
pid_t getppid(void) 获取父进程ID

例如:

#include 
#include 
#include 

int main()
{
    printf("PID = %d\n", getpid());
    printf("PPID = %d\n", getppid());

    while(1);

    return 0;
}

进程创建

1.fork()

pid_t fork(void)  创建子进程

子进程的ID号为父进程的ID号 + 1

特别:返回值返回两次,父进程返回子进程的ID号,子进程返回0

#include 
#include 
#include 
#include 
int main()
{
    pid_t pid;
    pid = fork();
    if(-1 == pid)
    {
        perror("fork");
        exit(1);
    }
    else if(0 == pid) //子进程返回0,子进程执行部分
    {
        printf("Child Process Id = %d\nParent Process Id = %d\n", getpid(), getppid());
    }
    else  //父进程执行部分
    {
        printf("Parent Process Id = %d\n", getpid());
    }

    return 0;
}

思考:下面这段代码的最终输出结果是???

#include 
#include 
#include 
int main()
{
    pid_t pid;
    pid = fork();
    int count = 0;
    count++;
    printf("%d\n", count);
    return 0;
}

注意:使用fork()创建进程后,会有两个进程存在,对于fork而言,这两个进程在不同的地址空间(之前我们讲过对于一个进程而言会有一个4G的虚拟内存),那么对于fork后的两个进程,会存在两个地址一模一样的空间,子进程会将父进程的所有代码(除去各自独有的代码)复制到自己的代码段。
写时复制:当某个进程访问某个变量,并要修改时,则会分配另一个空间。

对于上面这个情况,当子进程尝试count++时,会改变count的值,此时系统会开辟另一个空间给子进程,此时count为0,那么经过自加后,输出count为1;对于父进程而言,也是一样的。

所以,上面的代码最终结果是:

1
1

2.vfork()

 pid_t vfork(void);
  • vfork的子进程必须加exit()退出,否则会出错;
  • 子进程先运行,子进程运行后,再执行父进程
  • vfork的子进程与父进程共享相同的资源

下面这段代码的输出结果又是如何的呢?

#include 
#include 
#include 
#include 

int main()
{
    pid_t pid;
    int count = 0;
    pid = vfork();
    if(-1 == pid)
    {
        perror("vfork");
        exit(1);
    }
    else if(0 == pid)
    {
        count++;
        printf("count = %d\n", count);
        exit(1);
    }
    else
    {
        count++;
        printf("count = %d\n", count);
    }
    return 0;
}

我们知道,vfork的子进程和父进程共享相同的资源,那么在执行子进程中的count++时,count由原来的0变成1,输出1;
执行父进程的count++时,count又由1变成2,输出2;
所以最终结果是:

1
2

如果我将程序改动如下,结果又是什么呢?

......
int main()
{
    pid_t pid;
    pid = vfork();
    int count = 0;
    ......  
}

将pid = vfork();与int count = 0;的位置交换,最终结果会是如何?
结果如下:

1
1

为什么呢?
此时子进程和父进程依旧共享相同的资源,但它也共享了”int count = 0”这句话,所以在子进程执行完毕后,count = 1;当父进程开始执行时,先执行的是int count = 0,将原来的1又置为0,再经过自加操作,那么输出结果还是1;

3.exec函数族

exec启动一个新程序,替换原有的进程,因此进程的PID不会改变

1.int execl(const char *path, const char *arg, …);

path:被执行程序名(含完整路径)。
arg: 被执行程序所需的命令行参数,含程序名。以空指针(NULL)结束。

例如:
使用execl在进程中调用ls,显示当前目录下的文件信息。

#include 
#include 
#include 
#include 

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

    if(-1 == pid)
    {
        perror("vfork");
        exit(1);
    }
    else if(0 == pid)
    {
        printf("Child process:%d\n", getpid());
        execl("/bin/ls",NULL);
    }
    else
    {
        printf("Parent process\n");
    }
    return 0;
}

2.int execv(const char *path, char *const argv[]);
参数:
path:被执行程序名(含完整路径)。
argv[]: 被执行程序所需的命令行参数数组。

例如:

#include 
#include 
#include 

int main()
{
    char *argv[] = {"ls",NULL};
    execv("/bin/ls", argv);

    return 0;
}

进程等待

先介绍两个概念,一个是孤儿进程,一个是僵尸进程。

孤儿进程:其父进程在它之前结束,不能再对它进行回收

僵尸进程:子进程结束后,没有被回收的时间段内,被称为僵尸进程。

1.pid_t wait(int *status);

功能:阻塞该进程,直到其某个子进程退出。

例如:

#include 
#include 
#include 
#include 
#include 

int main()
{
    pid_t pid;
    pid = fork();
    int status;
    if(-1 == pid)
    {
        perror("fork");
        exit(1);
    }
    else if(0 == pid)
    {
        sleep(2);
        printf("Child Process\n");
        exit(6);
    }
    else
    {   
        printf("Parent Process\n");
        wait(&status);  //如果不需要知道它的退出类型,wait的形参写NULL也可以
        if(WIFEXITED(status))
        {
            printf("Exit Normally %d\n", WEXITSTATUS(status));
        }
    }
    return 0;
}

如果不写wait(),在子进程sleep的过程中,其父进程就已经结束了,那么子进程就会变成孤儿进程,但加上wait后,父进程直到子进程结束才会退出。

...
else if(0 == pid)
    {   
        printf("Child Process\n");
        exit(6);
    }
    else
    {   
        sleep(2);
        printf("Parent Process\n");
        wait(&status);  
        if(WIFEXITED(status))
        {
            printf("Exit Normally %d\n", WEXITSTATUS(status));
        }
    }
    ...

如果我们将子进程中的sleep函数放在父进程中,那么在子进程执行结束后,父进程需要沉睡两秒后才执行,这两秒内,父进程没有去回收子进程,所以称子进程为僵尸进程,但两秒钟后,子进程又被回收。

2.pid_t waitpid(pid_t pid, int *status, int options);

参数pid:(欲等待的子进程识别码)

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

参数option:(通常设置为0)

WNOHANG: 如果没有任何已经结束的子进程则马上返回,不予以等待。

WUNTRACED :如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。

进程退出

exit()与_exit()

exit():在停止进程之前,要检查文件的打开情况,并把文件缓冲区中的内容写回文件才停止进程。

_exit():直接使进程停止,清除其使用的内存,并清除缓冲区中内容

所以,我们通常选择使用exit(),而不是用_exit();

你可能感兴趣的:(文件编程)