栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 系统运维 > 运维 > Linux

第13章 多进程编程

Linux 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

第13章 多进程编程

13.1 fork系统调用

Linux创建新进程的系统调用是fork函数。

#include 
#include 

pid_t fork(void);
返回值:
每次调用都返回2次:
1、在父进程中返回子进程PID
2、在子进程中返回0
失败时返回-1,并设置errno

fork复制当前进程,复制后子进程跟父进程具有相同的内存空间,且代码与父进程完成相同。数据的复制采用的是写时复制,即只有在任一进程对数据执行了写操作时,复制才会发生。此外,创建子进程后,父进程中打开的文件描述符默认在子进程中也是打开的,且文件描述符的引用计数加1。父进程的用户根目录、当前工作目录等变量的引用计数均会加1。原进程设置的信号处理函数不再对新进程起作用。

#include 
#include 

int gval = 10;
int main()
{
    pid_t pid;
    int val = 10;
    pid = fork();
    if (pid == 0)   //子进程处理
    {
        val += 2;
    }
    else    //父进程
    {
        val -= 2;
    }
    if (pid == 0)
        printf("child proc, val = %dn", val);
    else
        printf("parent proc, val = %dn", val);
    
    return 0;
}

结果:
parent proc, val = 8
child proc, val = 12
13.2 exec系列系统调用

        fork创建子进程后执行的是和父进程相同的程序,子进程往往要调用一种exec函数来执行另一个程序。当进程调用一种exec函数后,该进程的用户空间代码和数据完全被新程序替换,exec函数后的代码不会再被执行。调用exec并不创建新进程,所以调用exec前后该进程的id不变。

#include 
extern char** environ;

//加载一个进程,通过 路径+程序名 来加载
最后一个参数必须是NULL,标记参数的结束
int execl(const char* path, const char* arg, ...);
如:execl("/bin/ls", "ls", "-l", NULL);

//借助PATH环境变量,加载一个进程
第一个参数无需给出具体的路径,只需给出函数名即可,系统会在PATH环境变量中寻找所对应的程序,如果没找到的话返回-1。最后一个参数必须是NULL,标记参数的结束
int execlp(const char* file, const char* arg, ...);
如:execlp("ls", "ls", "-l", NULL);    //file, arg[0](程序本身), arg[1], 

//l: 希望接收以逗号分隔的参数列表,列表以NULL指针作为结束标志
 e: 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
int execle(const char* path, const char* arg, ..., char* const envp[]);
如:execle("/bin/ls", "ls", "-a", NULL, NULL);

//execv中希望接收一个以NULL结尾的字符串数组的指针:char *argv[] = {"ls", "-a", "-l", NULL};
int execv(const char* path, char* const argv[]);
如:execv( "/bin/ls",arg)
int execvp(const char* file, char* const argv[]);
如:execvp("ls", argv);

//v: 希望接收到一个以NULL结尾的字符串数组的指针
  e: 函数传递指定参数envp,允许改变子进程的环境,无后缀e时,子进程使用当前程序的环境
int execve(const char* path, char* const argv[], char* const envp[]);
如:char *const ps_argv[] = {"ps", "-o", "pid, ppid, session, tpgid, comm, NULL"};
   char *const ps_envp[] = {"PATH=/bin:/usr/bin", "TERM=console", NULL};
   execve("/bin/ps", ps_argv, ps_envp);

        exec系列函数是不返回的,只有在出错时返回-1,并设置errno。如果没出错,原程序中exec调用之后的代码都不会执。exec系列函数的关系图如下:

13.3 处理僵尸进程

        僵尸进程:子进程结束,父进程没有回收子进程、释放子进程占用的内核资源,此子进程处于僵尸状态,被称为僵尸进程。

        父进程可以调用wait或waitpid函数来回收子进程,销毁僵尸进程。

13.3.1 wait函数--阻塞
#include 
#include 

//可以等待任意子进程终止
pid_t wait(int* status);
参数:
status:保存子进程的退出状态信息
返回值:成功时返回子进程ID,失败时返回-1并设置errno

sys/wait.h文件中定义了几个宏来帮助解释子进程的退出状态信息: 

         wait函数将阻塞父进程,直到该进程的子进程,结束为止。下面是使用wait的实例:

#include 
#include 
#include 
#include 

int main()
{
    int status;
    pid_t pid = fork();
    if (pid == 0)   
        return 3;
    else
    {
        printf("child PID : %dn", pid);
        wait(&status);
        if (WIFEXITED(status))
        {
            printf("child send one : %dn", WEXITSTATUS(status));
        }
    }
    return 0;
}
结果:
child PID : 124959
child send one : 3
13.3.2 waitpid函数--非阻塞
#include 
#include 

pid_t waitpid(pid_t pid, int* status, int options);
参数:
pid:等待终止的子进程的PID。若传-1,则与wait函数相同,可以等待任意子进程终止。
status:保存子进程的退出状态信息
options:控制waitpid函数的行为。常用值为WNONHANG,表示waitpid是非阻塞的。
返回值:成功时返回子进程ID(或0),失败时返回-1并设置errno

        waitpid函数是非阻塞的:如果pid指定的目标子进程还没有结束或意外终止,则waitpid立即返回0;如果目标子进程正常退出,则waitpid返回该子进程的PID。

#include 
#include 
#include 

int main()
{
    int status;
    pid_t pid = fork();
    if (pid == 0)  
    {
        sleep(5);
        return 100;
    } 
    else
    {
        while(!waitpid(-1, &status, WNOHANG))
        {
            sleep(3);
            puts("sleep 3s!");  //即使子进程没终止,也会打印这句话,因外waitpid的非阻塞性
        }
        if (WIFEXITED(status))
        {
            printf("child send %dn", WEXITSTATUS(status));
        }
    }
    return 0;
}
结果:
sleep 3s!
sleep 3s!
child send 100
13.4 管道

        管道是父进程和子进程间通信的常用手段。

        创建一根管道后,父子进程之间能够传递数据,利用的是fork调用后复制两个管道文件描述符,如下图所示:(管道和套接字一样,属于操作系统,并非属于进程的资源,因此,fork复制的并非管道,而是管道的文件描述符)

下面是利用pipe创建一跟管道进行父子进程通信的代码示例:

#include 
#include 
#define BUF_SIZE 30

int main()
{
    int fds[2];
    char str1[] = "who are you?";
    char str2[] = "I'm Lee!";
    char buf[BUF_SIZE] = {0};

    //创建一个管道,fd[0]读,fd[1]写
    pipe(fds);    //对于网络通信,也可以用socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
    pid_t pid = fork();
    if (pid == 0)
    {
        write(fds[1], str1, sizeof(str1));    //发送str1
        //sleep(2);
        read(fds[0], buf, BUF_SIZE);          //读取管道内的数据
        printf("child process output: %sn", buf);  //打印读取的数据
    }
    else
    {
        read(fds[0], buf, BUF_SIZE);
        printf("father process output: %sn", buf);
        write(fds[1], str2, sizeof(str2));
        //sleep(2);
    }
    return 0;
}
结果:
child process output: who are you?

实际运行的结果跟我么想象的不一样,是因为:数据进入管道后成为无主数据,子进程先是把数据发送到管道,接着又从管道读取数据,读取的时候不会区分这个数据是谁发的,只要管道内有数据就能读。想要使结果正常,就要把代码中的sleep注释放开。

        为了实现双向通信,可以创建2个管道,各自负责不同的数据流动即可:

实现代码如下:

#include 
#include 
#define BUF_SIZE 30

int main()
{
    int fds1[2], fds2[2];
    char str1[] = "who are you?";
    char str2[] = "I'm Lee!";
    char buf[BUF_SIZE] = {0};

    //创建2个管道
    pipe(fds1), pipe(fds2);
    pid_t pid = fork();
    if (pid == 0)
    {
        write(fds1[1], str1, sizeof(str1));
        read(fds2[0], buf, BUF_SIZE);
        printf("child process output: %sn", buf);
    }
    else
    {
        read(fds1[0], buf, BUF_SIZE);
        printf("father process output: %sn", buf);
        write(fds2[1], str2, sizeof(str2));
    }
    return 0;
}
结果:
father process output: who are you?
child process output: I'm Lee!

        不过,管道只能用于有关联的2个进程(如父子进程)间的通信。而system V IPC(消息队列,共享内存、信号量)能用于无关联的多个进程之间的通信,因为它们都使用一个全局唯一的键值来标识一条管道。但有一种特殊的管道名为FIFO,也叫命名管道,也能用于无关联进程之间的通信,但在网络编程中使用的不多。

13.5 信号量 13.5.1 信号量原语

        当多个进程同时访问系统上的某个资源时,需要考虑进程的同步问题,确保任意时刻只有一个进程可以拥有对资源的独占式访问。信号量实现了这一保护机制。

        信号量(semaphore)表示资源的数目,主要用于实现进程间的互斥与同步,它只能取自然数值并且只支持两种操作:等待(wait)和信号(signal)。由于在Linux中,等待和信号具有特殊含义,所以对信号量的这两种操作常被称呼为P、V操作,P、V操作是两种原子操作。V操作会增加信号量SV的数值,P操作会减少它。P操作是用在进入临界区之前,V操作是用在退出临界区之后,这两个操作必须成对出现。假设有信号量SV,则对它的P、V操作含义如下:

  • P(SV):如果SV的值大于0,就将它减1;如果SV的值为0,则挂起进程的执行。
  • V(SV):如果有其他进程因为等待SV而挂起,则唤醒之;如果没有,则将SV加1。

信号量跟信号没有任何关系。信号量又分为两种:二进制信号量和计数信号量。最常用、最简单的信号量是二进制信号量,只能取0和1两个值,二进制信号量又称为互斥锁。P、V操作解释如下:

P(sema)     //sema:信号量
{
    while(sema == 0);   //当信号量为0时,进行等待操作,此时进程被挂起,直到sema不为0
    sema = sema - 1;    //sema大于0,进程进入临界区,sema值减1
}
V(sema)
{
    sema = sema + 1;    //进程执行完任务,从临界区出来,sema的值增加1
}
13.5.2 信号量函数
功能:初始化一个信号量
int sem_init(sem_t *sem, int pshared, unsigned int value);
参1:sem信号量
参2:pshared取0用于线程间;取非0(一般为1)用于进程间
参3:value指定信号量初值

功能:以原子操作的方式将信号量的值减1 --
int sem_wait(sem_t *sem);              

功能:为sem_wait()的非阻塞版,不进行等待。如果信号量计数大于0,则信号量立即减1并返回0,否则立即返回-1
int sem_trywait(sem_t *sem);        

功能:与 sem_wait() 类似,只不过 abs_timeout 指定一个阻塞的时间上限。
int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); 
参2:abs_timeout采用的是绝对时间,由自1970-01-01 00:00:00 +0000(UTC) 秒数和纳秒数构成。
struct timespec {
    time_t tv_sec;        
    long   tv_nsec;       
};
sem_timedwait存在缺陷:假设当前系统时间是1565000000(2019-08-05 18:13:20),
sem_timedwait传入的阻塞等待的时间戳是1565000100(2019-08-05 18:15:00),
那么sem_timedwait就需要阻塞1分40秒(100秒),若在sem_timedwait阻塞过程中,
中途将系统时间往前修改成1500000000(2017-07-14 10:40:00),
那么sem_timedwait此时就会阻塞2年多!
返回值:
如果信号量大于0,则对信号量进行递减操作并立马返回正常;
如果信号量小于0,则阻塞等待,当阻塞超时时返回失败(errno 设置为 ETIMEDOUT)。
 

功能:以原子操作的方式将信号量的值加1 ++
int sem_post(sem_t *sem);            

功能:销毁一个信号量
int sem_destroy(sem_t *sem);   

功能:读取sem中信号量计数,放到sval指向的整数上
int sem_getvalue(sem_t * sem, int * sval);
    
所有这些函数在成功时都返回 0;错误保持信号量值没有更改,-1 被返回,并设置 errno 

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/868587.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号