Fork me on GitHub

Linux进程——进程创建和同步控制

Linux内核支持用户进程和内核进程两种进程。内核进程指完全运行在内核空间的进程,这种进程主要处理内核事务;用户进程一般运行在用户态,需要使用内核资源时,通过系统调用进入内核态,系统调用结束后,重新返回用户态。

创建进程

可通过fork函数创建子进程,理论上,父子进程拥有各自独立的用户空间。但Linux为了提高效率,采用COW(copy on write)算法。

fork函数原型如下:

/*
成功:子进程pid返回给父进程,0返回给子进程
失败: -1返回给父进程,设置errno
*/
pid_t fork();

案例:创建子进程

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

int glob = 10;

int main()
{
    int local;
    pid_t pid;
    local = 8;
    //向子进程的pid传值0
    if((pid = fork()) == 0)
    {   //子进程
        sleep(1);
        printf("i am in child process,%d\n",getpid());
    }zijc
    else
    {   //父进程
        printf("i am in father process,%d\n",getpid());
        glob++;
        local--;
        sleep(5);
    }
    printf("pid = %d,glob = %d,localar = %d\n",getpid(),glob,local);
    return 0;
}

/*输出为:
i am in father process,13023
i am in child process,13024
pid = 13024,glob = 10,localar = 8
pid = 13023,glob = 11,localar = 7
*/

当用fork()函数创建子进程时,Linux内核为子进程分配一个进程控制块task_struct。子进程的进程控制块用来存放子进程拥有的资源、管理信息和进程状态等。

此时,在父子进程没有对数据进行读写操作之前,父子进程共享用户地址空间。当父进程执行glob++,Linux内核采用COW算法,首先为子进程创建相应的数据区,接着内核将父进程地址空间中的数据区相关页复制到子进程地址空间中数据区的相关页,此时,父子进程各自拥有独立的全局变量glob。当执行local--语句,内核以同样方法在子进程用户地址空间的栈区的相应页建立复制。而代码区是只读的,所以父子进程共享代码区,直接建立映射,不进行复制。

程序启动和结束

初始化程序

在加载可执行文件后,首先运行的是称为start-up的代码,此部分代码在程序链接为可执行程序时,由链接器加入,作用是从内核读取进程运行的环境信息,如环境变量、命令行参数等。

start-up完成初始化工作后,调用main函数,执行完进程后,通过exit函数结束进程。

结束进程

每个进程都有父进程,当子进程运行结束后,子进程进程僵尸状态,并向父进程发送SIGCHLD信号,通知子进程已经终止。在该状态下子进程几乎释放了所有内存资源,不能被重新调度,仅在进程列表中保留一个位置,只保留进程如何终止的一些状态信息,以供回收者使用。父进程可以通过调用wait或waitpid函数获取子进程的退出码,以便判断子进程结束的原因。由父进程释放子进程余下的所有资源。

但当父进程在子进程之前终止,子进程的父进程将更改为init进程,由init进程负责子进程的善后处理工作。

//终止进程,status返回值
void exit(int status);
/*
登记终止处理函数,ANSI C规定,一个进程可以登记最多32个终止处理函数,这些函数由exit自动调用。exit以先进后出的方式调用atexit登记的函数,同一函数登记多次,也被调用多次。
根据ANSI C,exit首先调用终止处理函数,然后按需调用fclose,关闭所有打开的文件流,保证基于缓冲区的文件I/O操作完整性。
这样,在进程结束前,将未写入文件的缓冲区数据,通过exit函数进行保存。
func终止处理函数
成功返回0,否则非0
*/
int atexit(void(*func)(void));
//直接结束进程,不进行任何其他处理
void _exit(int status);

案例:直接退出进程

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

int main()
{
    printf("output begin\n");
    printf("content in buffer");
    printf("drop the buffer");
    _exit(0);
}

案例:登记终止处理函数

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

static void my_exit1(void)
{
    printf("first exit handler\n");
}

static void my_exit2(void)
{
    printf("second exit handler\n");
}

int main(void)
{
    if(atexit(my_exit2) != 0)
        printf("can't register my_exit2");
    if(atexit(my_exit1) != 0)
        printf("can't register my_exit1");
    if(atexit(my_exit1) != 0)
        printf("can't register my_exit1");
    printf("main is done\n");
    return 0;
}

进程同步控制

当创建一个子进程后,父子进程的执行顺序无法控制。当父子进程同事操作共享资源,不同的执行次序有可能导致不同的运行结果,从而出现数据不一致性。为解决这一问题,必须提供进程间的同步控制机制。

wait和waitpid可用来实现父子进程同步,用来等待子进程结束。 wait函数的功能是获取子进程如何终止的信息,清除子进程的剩余资源。父进程调用wait函数,进入阻塞队列,等待某个子进程的结束。当子进程结束,会产生结束状态字status,并向父进程发送SIGCHLD信号。 父进程收到SIGCHLD信号,若希望知道子进程结束状态,调用wait,否则忽略该信号。

/*
暂停执行,将子进程结束状态写入status中,并确认子进程已经结束
status 子进程状态
成功返回子进程PID,否则返回-1
*/
pid_t wait(int *status);
/*
等待指定子进程结束
pid  指定等待的子进程
    <-1 pid所代表进程组中进程
    -1 任何子进程
    0 与该进程同组的进程
    >0 进程标识符为pid的进程
status 保存子进程状态
options 等待方式
    WNOHANG 进程不阻塞
    WUNTRACED 当有子进程结束时返回
*/
pid_t waitpid(pid_t pid,int *status,int options);

案例:依次等待多个子进程结束,并显示结束状态

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

int main(void)
{
    pid_t pid[10],wpid;
    int child_status,i;
    //创建5个子进程
    for(i=0;i<5;i++)
    {
        if((pid[i]=fork()) == 0)
            exit(100+i);
    }
    //等待子进程结束,输出status
    for(i=0;i<5;i++)
    {
        wpid = waitpid(pid[i],&child_status,0);
        if(WIFEXITED(child_status))
            printf("Child %d exit with status %d\n",
                wpid,WEXITSTATUS(child_status));
        else
            printf("Child %d terminated abnormally\n",wpid);
    }
    return 0;
}

Comments