第三天----进程

进程的控制

生成子进程

system函数:
a. #include
b. 可以使用该函数,根据传入的命令启动一个进程
c. 返回值:成功返回0 不支持shell返回-1 失败返回非0

#include 
#include 
int main()
{

   int ret = system("ping -c 10 www.baidu.com");
   if(ret!=0)
   {
       perror("system");
       return -1;
   }
   return 0;
}

主题:ps -ef查看子进程
system函数用到的系统调用为fork、execve和waitpid

fork函数
a. 可以复制当前父进程,从而生成一个子进程。子进程的资源与父进程相同。缓冲区,socket,文件描述符等
b. 返回值:0代表子进程,-1代表失败,>0代表子进程pid
c. #include#include
对于fork来说,子进程与父进程的运行顺序是不一定的。

  1. getpid函数
    a. 返回本进程的pid
  2. getppid函数
    a. 返回父进程的pid
  3. sleep函数
    a. #include
    b. 整个进程睡眠指定秒数,如果在sleep()期间进程接收到信号,且信号有相应的处理函数,则sleep()可能提前结束,并返回剩余未休眠的时间
    c. 返回值:如果sleep()函数正常执行且休眠时间结束,则返回0;如果sleep()由于接收到信号而被提前唤醒,则返回剩余的未休眠秒数
#include 
#include 
#include 
#include 
#include 
int main()
{
   int fd = open("test.txt",O_WRONLY);
   pid_t pid = fork();
   if(pid==-1)
   {
       perror("fork");
       return -1;
   }
   else if(pid==0)
   { //
       char buf[1]={'a'};
       for(int i=0;i<10;i++)
       {
           write(fd,buf,sizeof(buf));
           buf[0]++;
       }
   }
   else
   {
       char buf[1]="1";
       for(int i=0;i<10;i++)
       {
           write(fd,buf,sizeof(buf));
           buf[0]++;
       }
   }
   return 0;
}

对于这段代码来说,我们写入文件的顺序是随机的,也就是说是数字和字母混着写的,这代表着,当我们使用fork复制父进程生成子进程的时候,我们会复制一份与父进程相同的文件描述符表,但是他们共用一份文件描述struct file,也就是说,对于父子进程来说,文件的偏移量等信息是相同的。

#include 
#include 
#include 
#include 
#include 
int main()
{
    pid_t pid = fork();
    int fd = open("test.txt",O_WRONLY);
    if(pid==-1)
    {
        perror("fork");
        return -1;
    }
    else if(pid==0)
    { //
        char buf[1]="1";
        write(fd,buf,sizeof(buf));
    }
    else
    {
        char buf[1]={'a'};
        write(fd,buf,sizeof(buf));
    }
    return 0;
}

这段代码的结果是在文件中写入1或者a,这代表着在两个子进程中,我们通过open打开的文件描述,有着各自的文件描述符表以及文件描述struct file。并且对于一个文件来说,它可以同时被两个子进程打开并且进行修改。

execve函数
exec系列函数可以在同一个进程中跳转执行另外一个程序。
函数参数:
● char *__path:绝对路径
● char const argv[]:程序的参数列表
○ (1) 需要执行的程序命令(同
__path)
○ (2) 执行程序需要传入的参数
○ (3) 最后一个参数必须是NULL
● char *const __envp[]:指向字符串数组的指针需要传入多个环境变量参数
○ (1) 环境变量参数固定格式 key=value
○ (2) 最后一个参数必须是NULL

#include 
#include "unistd.h"
#include "sys/types.h"
#include "fcntl.h"

int main()
{
   printf("我是父进程,pid是%d\n",getpid());
   char *const argv[] = {"/home/wangze/桌面/Unix/第三章/process_del","我是参数",NULL};
   char *const __envp[] = {"PATH=/home/wangze/bin:/home/wangze/.local/bin:/home/wangze/bin:/home/wangze/.local/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin",NULL};

   int ret = execve(argv[0],argv,__envp);
   if(ret==-1)
   {
       perror("execve");
   }

   return 0;
}
#include 
#include 
#include 
#include 
#include 
int main(int argc, char const *argv[])
{
    
    printf("我是子进程,接收到的参数是%s,pid是%d\n",argv[1],getpid());
    return 0;
}

execve函数经常与fork函数连用,使用场景如下,我们可以先使用fork生成一个子线程,父进程继续处理自己的逻辑,子线程如果满足某种条件,则可以立刻结束并且调用一个新的进程。

fork......
if 父进程:
    继续执行
else if 子进程:
    调用新的进程execve
waitpid

Linux中父进程除了可以启动子进程,还要负责回收子进程的状态。如果子进程结束后父进程没有正常回收,那么子进程就会变成一个僵尸进程——即程序执行完成,但是进程没有完全结束,其内核中PCB结构体没有释放。
waitpid这个函数可以让父进程等待子进程运行结束后再执行父进程。
#include#include
pid_t waitpid(pid_t pid,int *wstatus, int options);
● pid:等待模式
○ -1:等待任何子进程终止,并返回最先终止的子进程的ID
○ 0:儿子子进程的终止
○ 大于0:等待指定pid的进程终止
● wstatus:证书指针,子进程返回的状态码会保存到该int
● options:
○ WNOHANG :如果没有子进程终止,也立刻返回,通常用于查看子进程状态,而非等待
○ WUNTRACED:收到子进程处于收到信号停止的状态也返回
通常使用:waitpid(pid,&subprocess_status,0);
pstree -p 查看进程树

孤儿进程
父进程先于子进程结束,子进程就成为了孤儿进程。
孤儿进程会被其祖先自动领养。此时的子进程因为和终端切断了联系,所以很难再进行标准输入使其停止了,所以写代码的时候一定要注意避免出现孤儿进程。

进程间通信

匿名管道Pipe
● #include
int pipe(int pipefd[2]);

pipe函数用于在内核空间创建管道,用于父子进程或者其他相关联的进程之间通过管道进行双向的数据传输。
pipefd: 用于返回指向管道两端的两个文件描述符。pipefd[0]指向管道的读端。pipefd[1]指向管道的写端。
● return
○ 成功:0
○ 不成功:-1,并且pipefd不会改变
常用宏定义

#define EXIT_FAILURE    1   /* Failing exit status.  */
#define EXIT_SUCCESS    0   /* Successful exit status.  */
这些退出状态可以直接作为_exit()函数的参数。

#define STDIN_FILENO    0   /* Standard input.  */
#define STDOUT_FILENO   1   /* Standard output.  */
#define STDERR_FILENO   2   /* Standard error output.  */
#include 
#include 
#include 
#include 
#include 
#include 
int main()
{
    int pipe_fd[2]={0};
    __pid_t cpid;
    int ret = pipe(pipe_fd);
    if(ret==-1)
    {
        perror("pipe:");
    }

    cpid = fork();
    if(cpid<0)
    {
        perror("fork:");
    }
    else if(cpid==0)
    {
        // 子进程读取
        close(pipe_fd[1]); // close write 
        char buf[128]={0}; 
        printf("children is running\n"); 
        while(read(pipe_fd[0],buf,sizeof(buf))>0)
        {
            write(STDOUT_FILENO,buf,strlen(buf));    
        }  
        printf("\n");
        close(pipe_fd[0]);
        _exit(EXIT_SUCCESS);
    }
    else
    {
        // 父进程写入
        close(pipe_fd[0]);
        char buf[128]="this is father send data";
        write(pipe_fd[1],buf, strlen(buf));
        close(pipe_fd[1]); // close read
        waitpid(cpid,NULL,0);
        exit(EXIT_SUCCESS);
    }
    return 0;
}

注意:
对于子进程的退出,要使用_exit()函数,对于父进程的退出,要使用exit()函数
必须在fork之前进行pipe,否则会导致失败。
● pipe修改的pipe_fd,pipe_fd[0]代表读,pipe_fd[1]代表写。
在使用的使用,一端只能够使用一个文件描述符,也就是说,pipe_fd在一端只能读或者只能写,另一个必须关闭。
● 在使用完后要关闭使用的文件描述符
● 父进程在write之后立即关闭了写端,这会触发子进程read返回0
● 管道返回的两个文件描述符分别表示读写,各自指向一个struct file结构体,然而,它们并不对应真正的文件。struct file的私有数据指针属性private_data指向struct pipe_inode_info类型的结构体。
问题:
我们已经知道,read函数会直接读取文件中的内容,并且返回读取到的字节数,如果文件为空则立刻返回0。
如果我们在父进程中sleep(1),然后再执行代码会发生什么?

else
    {
        sleep(1);
        // 父进程写入
        close(pipe_fd[0]);
        char buf[128]="this is father send data";
        write(pipe_fd[1],buf, strlen(buf));
        close(pipe_fd[1]); // close read
        waitpid(cpid,NULL,0);
        exit(EXIT_SUCCESS);
    }

其实对于上面的问题,该段代码都可以正常执行,这是为什么呢?在这段代码中,显然,子进程read的时候,父进程还没有将数据放到管道中,因此read应该立刻返回0,并且不再打印buf的内容,但是为什么子进程正常打印呢?
对于read来说,如果read读取的是普通的文件,当文件为空的时候,read会立刻返回0,但是如果read读取的是管道,或者套接字这种类型的文件,read则会阻塞,直到有数据可以读取之后,再进行返回。因此代码可以正常执行。

如果 while(read(pipe_fd[0],buf,sizeof(buf))>0)这行代码不使用while会发生什么?

  1. 数据量可能大于缓冲区大小:如果父进程写入的数据超过子进程缓冲区的大小(比如128字节),一次read可能无法读取全部数据,需要循环读取直到读完所有数据。
  2. 确保读取所有数据直到EOF:当写端关闭时,read返回0,循环终止。这样即使父进程分多次写入,子进程也能完整读取所有数据,直到管道被关闭。
  3. 处理可能的多次写入:虽然在这个例子中父进程只写入一次,但实际应用中可能有多次写入,循环读取保证处理所有数据。

使用管道的限制:
(1)两个进程通过一个管道只能实现单向通信,比如上面的例子,父进程写子进程读,如果有时候也需要子进程写父进程读,就必须另开一个管道。
(2)管道的读写端通过打开的文件描述符来传递,因此要通信的两个进程必须从它们的公共祖先那里继承管道文件描述符。上面的例子是父进程把文件描述符传给子进程之后父子进程之间通信,也可以父进程fork两次,把文件描述符传给两个子进程,然后两个子进程之间通信,总之需要通过fork传递文件描述符使两个进程都能访问同一管道,它们才能通信。

有名管道FIFO
对于匿名管道来说,必须要是父子进程之间才能够进行通信,但是对于有名管道来说,任何进程之间都可以进行通信。但要注意的是,无论是有名管道还是匿名管道,同一条管道只应用于单向通信。
● #include#include
int mkfifo(const char *pathname, mode_t mode);
● 返回值:成功返回0,失败返回-1
用于创建有名管道。该函数可以创建一个路径为pathname的FIFO专用文件,mode指定了FIFO的权限,FIFO的权限和它绑定的文件是一致的。FIFO和pipe唯一的区别在于创建方式的差异。一旦创建了FIFO专用文件,任何进程都可以像操作文件一样打开FIFO,执行读写操作。

#include 
#include 
#include 
#include 
#include 
#include 
#include 
// send port
int main()
{
    char *path = "/tmp/myfifo";
    if(0!=mkfifo(path,0664))
    {
        perror("mkfifo:");
        if(errno!=17) //为了防止因为重复创建mkfifo而无法执行函数
        {
            exit(EXIT_FAILURE); 
        }
        
    }
    pid_t cpit;
    ssize_t readBytes;
    char buf[128]={0};
    int mkfifofd = open(path,O_WRONLY);

    if(mkfifofd<0)
    {
        perror("mkfifofd");
        exit(EXIT_FAILURE);
    }

    // STDIN have data then 
    while((readBytes = read(STDIN_FILENO,buf,sizeof(buf)))>0)
    {
        write(mkfifofd,buf,readBytes);
    }
    
    if(readBytes<0)
    {
        perror("read");
        exit(EXIT_FAILURE);
    }
    printf("发送管道退出,进程终止\n");
    close(mkfifofd);
    // 关于unlik函数,看下面的注解
    if(unlink(pipe_path) == -1) {
        perror("fifo_write unlink");
    }
    return 0;
}
#include 
#include 
#include 
#include 
#include 
#include 
#include 
// send port
int main()
{
    char *path = "/tmp/myfifo";
    pid_t cpit;
    ssize_t readBytes;
    char buf[128]={0};
    int mkfifofd = open(path,O_RDONLY);

    if(mkfifofd<0)
    {
        perror("mkfifofd");
        exit(EXIT_FAILURE);
    }

    // STDIN have data then 
    while((readBytes = read(mkfifofd,buf,sizeof(buf)))>0)
    {
        write(STDOUT_FILENO,buf,readBytes);
    }
    
    if(readBytes<0)
    {
        perror("read");
        exit(EXIT_FAILURE);
    }
    close(mkfifofd);
    return 0;
}

有名管道使用完成后,应该通过unlink调用清除相关资源。这个函数只能调用一次,重复清除会提示No such file or directory
● #include
int unlink(const char*pathname);
● return:成功返回0,失败返回-1,并设置errno
从文件系统中清除一个名称及其链接的文件
注意:
/tmp/myfifo,这实际上就是fifo专用文件在文件系统的路径
● 打开有名管道时,flags只应为O_WRONLY或O_RDONLY。
● 内核为每个被进程打开的FIFO专用文件维护一个管道对象。当进程通过FIFO交换数据时,内核会在内部传递所有数据,不会将其写入文件系统。因此,/tmp/myfifo文件大小始终为0
● 文件详细信息最开头的字母p表示这个是一个有名管道文件。

共享内存
首先,我们要学习两个函数shm_open() shm_unlink()。

  1. shm_open()
    shm_open可以开启一块内存共享对象,我们可以像使用一般文件描述符一样使用这块内存。
    ● #include
    int shm_open(const char *name, int oflag, mode_t mode);
    ● 返回值:成功执行返回一个文件描述符,失败返回-1
    oflag的模式:O_CREATO_EXCLO_RDONLYO_RDWRO_TRUNC
    ● O_CREATO_EXCL这两个通常合并使用,用来表示如果共享内存对象已经存则,则返回错误(避免覆盖)
    ● O_TRUNC用于阶段现有对象至0,只有当打开模式中有O_RDWR的时候才有效。

  2. shm_unlik()
    shm_unlik用来删除一个先前由shm_open创建的共享内存对象,这个函数仅仅是移除了与共享内存对象关联的名称,使得无法通过该名称打开共享内存。只有当所有打开该共享内存的进程关闭他们的描述符后,系统才会真正的释放内存资源。
    int shm_unlik(const char *name);
    ● 返回值:成功返回0,失败返回-1;

  3. truncate()和ftruncate()
    这两个函数都可以将文件缩放到指定大小,如果缩小,则截断数据,如果放大,则扩展部分为\0,缩放前后文件的偏移量不会更改
    ● #include#include
    int ftruncate(int fd, off_t length);
    int truncate(const char *path, off_t length);
    ● 返回值:成功返回0,失败返回-1
    不同的是,前者需要指定路径,后者需要提供文件描述符。ftruncate的对象可以是shm_open开启的内存对象,truncate缩放的文件必须是文件系统已存在的文件,否则会失败。

  4. mmap()
    mmap可以将一组设备或者文件映射到内存地址,我们在内存中寻址,就相当于在读取这个文件指定地址的数据,父进程在创建一个内存共享对象并且将其映射到内存区域后,子进程就可以正常读写该内存区,并且父进程也能看到更改。
    ● #include
    void *mmap(void *addr,size_t length,int prot,int flags,int fd,off_t offset);
    ○ addr:通常为NULL,让系统自动选择合适的起始地址
    ○ length:映射的内存区域长度,以字节为单位
    ○ prot:内存映射区域的保护标志,PROT_READPROT_WRITEPROT_EXECPROT_NONE
    ○ flags:MAP_SHAREDMAP_PRIVATEMAP_ANONYMOUSMAP_FIXED,一般使用第一个
    ○ fd:指定要映射的文件或设备
    ○ offset:从文件开头的偏移量,映射开始的位置
    ● 返回值:void* 成功的时候则返回映射区域的起始地址,失败则返回(void*)-1,并设置errno
    int munmap(void *addr, size_t length);
    用来取消映射的地址,addr是mmap的返回值,length必须和mmap的地址长度大小相同。

#include 
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    char *share;
    int fd;
    pid_t pid;
    char *shmName = "/letter";
    fd = shm_open("/letter", O_CREAT | O_RDWR, 0644);
    if(fd<0)
    {
        perror("shm_open");
        exit(EXIT_FAILURE);
    }
    ftruncate(fd,100);
    share = mmap(NULL,100,PROT_READ|PROT_WRITE,MAP_SHARED,fd,0);
    if(share==MAP_FAILED)
    {
        perror("mmap");
        exit(EXIT_FAILURE);
    }
    close(fd);

    pid = fork();
    if(pid<0)
    {
        perror("fork:");
        exit(EXIT_FAILURE);
    }
    else if(pid==0)
    {
        // child
        strcpy(share,"data\n");
    }
    else
    {
        // father
        sleep(1);
        printf("%s",share);
        wait(NULL);
        int ret = munmap(share,100);
        if(ret==-1)
        {
            perror("munmap");
            exit(EXIT_FAILURE);
        }
    }
    shm_unlink(shmName);
    return 0;
}

gcc 的时候要链接 -lrt
分步骤来说的话:

  1. shm_open
  2. ftruncate
  3. mmap
  4. fork
  5. munmap
  6. shm_unlink

临时文件系统:
Linux的临时文件系统(tmpfs)是一种基于内存的文件系统,它将数据存储在RAM或者在需要时部分使用交换空间(swap)。tmpfs访问速度快,但因为存储在内存,重启后数据清空,通常用于存储一些临时文件。
我们可以通过df -h查看当前操作系统已挂载的文件系统。
● 内存共享对象在临时文件系统中的表示位于/dev/shm目录下。

消息队列
消息队列也是进程间的一种通信方式。
相关数据类型:mqd_t,struct mq_attr,struct timespec

  1. mq_open()
    ● #include #include #include
    ● mqd_t mq_open(const char name, int oflag, mode_t mode,struct mq_attrattr);
    ● name:以/开头,以\0结尾的字符串
    ● oflag:O_RDONLYO_WRONLYO_RDWRO_CLOEXECO_CREATO_EXCLO_NONBLOCK
    ● mode:每个消息队列在mqueue文件系统中对应一个文件,mode是用来指定消息队列对应文件的权限的
    ● attr:属性信息,如果为NULL,则队列以默认属性创建
    用来创建或者打开一个已经存在的消息队列,消息队列通过名称name唯一标识
    当没有O_CREATE的时候,可以不填写mode,attr两个参数
  2. mq_timesend()
    ● int mq_timedsend(mqd_t mqdes, const char *msg_ptr, size_t msg_len, unsigned int msg_prio, const struct timespec *abs_timeout);
    将msg_ptr指向的消息追加到消息队列描述符mqdes指向的消息队列的尾部,如果消息队列已满,默认情况下,调用阻塞直至有充足的空间允许新的消息入队,或者达到abs_timeout指定的等待时间节点,或者调用被信号处理函数打断。需要注意的是,正如上文提到的,如果在mq_open时指定了O_NONBLOCK标记,则转而失败。
  3. mq_timedreceive()
    ● ssize_t mq_timedreceive(mqd_t mqdes, char *msg_ptr, size_t msg_len, unsigned int *msg_prio, const struct timespec *abs_timeout);
    从消息队列中取走最早入队且权限最高的消息,将其放入msg_ptr指向的缓存中。如果消息队列为空,默认情况下调用阻塞
  4. int mq_unlink(const char *name);
    ● 返回值:成功返回0,失败返回-1
    清除name对应的消息队列,mqueue文件系统中的对应文件被立即清除。消息队列本身的清除必须等待所有指向该消息队列的描述符全部关闭之后才会发生。
  5. clock_gettime()
    ● int clock_gettime(clockid_t clockid, struct timespec *tp);
    获取以struct timespec形式表示的clockid指定的时钟
#include 
#include 
#include 
#include 
#include 
#include 

// producer
int main()
{
    char *path = "/letter_queue";
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 100;
    attr.mq_curmsgs = 0;


    mqd_t fd = mq_open(path,O_CREAT|O_WRONLY,0664,&attr);
    if(fd<0)
    {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }
    char buf[100];
    while(1)
    {
        memset(buf,0,sizeof(buf));
        struct timespec timeinfo;
        clock_gettime(CLOCK_REALTIME, &timeinfo);
        timeinfo.tv_sec+=5;
        int read_count = read(STDIN_FILENO,buf,sizeof(buf));
        if(read_count==-1)
        {
            perror("read");
            exit(EXIT_FAILURE);
        }
        else if(read_count==0) 
        {
            char ch = EOF;
            printf("send EOF\n");
            if(-1==mq_timedsend(fd,&ch,sizeof(ch),0,&timeinfo))
            {
                perror("mq_timedsend");
            }
            break;
        }
        else
        {
            if(-1==mq_timedsend(fd,buf,sizeof(buf),0,&timeinfo))
            {
                perror("mq_timedsend");
            }
            printf("data send succeed\n");
        } 
    }
    close(fd);
    return 0;
}
#include 
#include 
#include 
#include 
#include 
#include 

int main()
{
    char *path = "/letter_queue";
    struct mq_attr attr;
    attr.mq_flags = 0;
    attr.mq_maxmsg = 10;
    attr.mq_msgsize = 100;
    attr.mq_curmsgs = 0;

    mqd_t fd = mq_open(path,O_RDONLY|O_CREAT,0664,&attr);
    if(fd==-1)
    {
        perror("mq_open");
        exit(EXIT_FAILURE);
    }
    while(1)
    {
        char buf[100];
        struct timespec time_info;
        clock_gettime(CLOCK_REALTIME, &time_info);
        time_info.tv_sec += 86400;

        memset(buf,0,sizeof(buf));
        
        if(mq_timedreceive(fd,buf,sizeof(buf),NULL,&time_info)==-1)
        {
            perror("mq_timdreceive");
            exit(EXIT_FAILURE);
        }
        else{
            if(buf[0]==EOF)
            {
                printf("send is end");
                break;
            }
            printf("recv data is: %s",buf);
        }
    }
    
    close(fd);
    if(-1==mq_unlink(path))
    {
        perror("mq_unlink");
        exit(EXIT_FAILURE);
    }
    return 0;
}

● 注意:
○ mq_unlink函数只能使用一次,我们选择在接受代码中使用,因为如果接受代码退出了,那么代表发送终端已经按下了ctrl+D,因此我们此时可以解除mq_unlink的链接的。
○ clock_gettime(CLOCK_REALTIME, &time_info);这行代码可以获取当前时间,并且保存到time_info中,CLOCK_REALTIME是一个time.h中带的宏
○ 当终端按下Ctrl+D的时候,代表结束从屏幕的输入流,此时read(stdin)将会返回0,因此就可以运行我们发送EOF的逻辑。

总结进程间通信方式的优缺点

  1. 匿名管道,有名通道:二者适合发送单条消息数据量小,但是频率高的场景。
  2. 共享内存:共享内存适合进程间消息发送数据量大,且要求速度快效率高的场景
  3. 消息队列

信号

在这里,我们仅仅是简单的讲解一下信号的基础知识点
每种信号都有其特定的含义和行为,进程可以通过注册信号处理函数来捕获信号并执行相应的操作,例如终止进程、忽略信号或者执行特定的处理逻辑。如果想查看所有的Linux信号,请执行kill -l指令,会得到。
我们可以通过signal系统调用注册信号处理函数。

● #include 
● sighandler_t signal(int signum, sighandler_t handler);
sighandler_t是函数指针,用来标识信号处理函数
#include 
#include 
#include 
#include 

void sigint_handler(int signum)
{
    printf("\n recv data:%d\n",signum);
    exit(signum);
}
int main()
{
    if(signal(SIGINT,sigint_handler)==SIG_ERR)
    {
        perror("signal");
        return 1;
    }
    while(1)
    {
        sleep(1);
        printf("now i am wait\n");
    }
    return 0;
}

当我们在运行代码后,在终端按下ctrl c的之后,就可以实现向程序发送信号。

你可能感兴趣的:(嵌入式应用开发,linux)