Loading

关于C语言进程操作

关于C语言进程操作

Linux标准库 <unistd.h>

符号常量

NULL		// Null pointer
SEEK_CUR	// Set file offset to current plus offset.
SEEK_END	// Set file offset to EOF plus offset.
SEEK_SET	// Set file offset to offset.

是POSIX标准定义的unix类系统定义符号常量的头文件,包含了许多UNIX系统服务的函数原型,例如read函数、write函数和getpid函数。

unistd.h在unix中类似于window中的windows.h。

#ifdef WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif

函数原型

ssize_t      read(int, void *, size_t);
int          unlink(const char *);
ssize_t      write(int, const void *, size_t);
int          usleep(useconds_t);
unsigned     sleep(unsigned);
int          access(const char *, int);
unsigned     alarm(unsigned);
int          chdir(const char *);
int          chown(const char *, uid_t, gid_t);
int          close(int);
size_t       confstr(int, char *, size_t);
void        _exit(int);
pid_t        fork(void);

关于管道pipe

管道的概念

管道是一种最基本的IPC机制,作用于有血缘关系的进程之间,完成数据传递。调用pipe系统函数即可创建一个管道。有如下特质:

  1. 其本质是一个伪文件(实为内核缓冲区)
  2. 由两个文件描述符引用,一个表示读端,一个表示写端。
  3. 规定数据从管道的写端流入管道,从读端流出。

管道的原理

管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。

管道的局限性

  1. 数据自己读不能自己写。
  2. 数据一旦被读走,便不在管道中存在,不可反复读取。
  3. 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
  4. 只能在有公共祖先的进程间使用管道。

常见的通信方式有,单工通信、半双工通信、全双工通信。

pipe 函数

创建管道

int pipe(int pipefd[2]); 成功:0;失败:-1,设置errno

函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。

管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:

geekfx

  1. 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
  2. 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
  3. 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。

管道的读写行为

​ 使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):

  1. 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
  2. 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
  3. 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
  4. 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。

总结

  1. 读管道:

    1. 管道中有数据,read返回实际读到的字节数。

    2. 管道中无数据:

      1. 管道写端被全部关闭,read返回0 (好像读到文件结尾)
      2. 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
  2. 写管道:

    1. 端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
    2. 管道读端没有全部关闭:
      1. 管道已满,write阻塞。
      2. 管道未满,write将数据写入,并返回实际写入的字节数。

当管道进行写入操作的时候,如果写入的数据小于128K则是非原子的,如果大于128K字节,缓冲区的数据将被连续地写入管道,直到全部数据写完为止,如果没有进程读取数据,则将一直阻塞。

命名管道FIFO

管道最大的劣势就是没有名字,只能用于有一个共同祖先进程的各个进程之间。FIFO代表先进先出,单它是一个单向数据流,也就是半双工,和管道不同的是:每个FIFO都有一个路径与之关联,从而允许无亲缘关系的进程访问。

关于 stdin、stdout 和 STDOUT_FILENO、STDIN_FILENO

在UNIX系统调用中,标准输入描述字用stdin,标准输出用stdout,标准出错用stderr表示,但在一些调用函数,引用了STDIN_FILENO表示标准输入才,同样,标准出入用STDOUT_FILENO,标准出错用STDERR_FILENO

stdin等是FILE *类型,属于标准I/O,在<stdio.h>

STDIN_FILENO等是文件描述符,是非负整数,一般定义为0, 1, 2,属于没有buffer的I/O,直接调用系统调用,在<unistd.h>

关于 perror

perror(s)用来将上一个函数发生错误的原因输出到标准设备(stderr)。参数s所指的字符串会先打印出,后面再加上错误原因字符串。此错误原因依照全局变量errno的值来决定要输出的字符串。

在库函数中有个errno变量,每个errno值对应着以字符串表示的错误类型。当你调用"某些"函数出错时,该函数已经重新设置了errno的值。perror函数只是将你输入的一些信息和errno所对应的错误一起输出。

关于 lockf

lockf()函数允许将文件区域用作信号量(监视锁),或用于控制对锁定进程的访问(强制模式记录锁定)。试图访问已锁定资源的其他进程将返回错误或进入休眠状态,直到资源解除锁定为止。当关闭文件时,将释放进程的所有锁定,即使进程仍然有打开的文件。当进程终止时,将释放进程保留的所有锁定。

int lockf(int fd, int cmd, off_t len);
  • fd是打开文件的文件描述符
  • cmd是指定要采取的操作的控制值,允许的值在中定义
    • F_ULOCK 0 //解锁
    • F_LOCK 1 //互斥锁定区域
    • F_TLOCK 2 //测试互斥锁定区域
    • F_TEST 3 //测试区域

F_ULOCK请求可以完全或部分释放由进程控制的一个或多个锁定区域。如果区域未完全释放,剩余的区域仍将被进程锁定。如果该表已满,将会返回[EDEADLK]错误,并且不会释放请求的区域。

使用F_LOCKF_TLOCK锁定的区域可以完全或部分包含同一个进程以前锁定的区域,或被同一个进程以前锁定的区域包含。此时,这些区域将会合并为一个区域。如果请求要求将新元素添加到活动锁定表中,但该表已满,则会返回一个错误,并且不会锁定新区域。

F_LOCKF_TLOCK请求仅在采取的操作上有所差异(如果资源不可用)。如果区域已被其他进程锁定,F_LOCK 将使调用进程进入休眠状态,直到该资源可用,而F_TLOCK则会返回[EACCES]错误。

F_TEST用于检测在指定的区域中是否存在其他进程的锁定。如果该区域被锁定,lockf()将返回 -1,否则返回0;在这种情况下,errno设置为[EACCES]。F_LOCKF_TLOCK都用于锁定文件的某个区域(如果该区域可用)。F_ULOCK用于删除文件区域的锁定。

  • len是要锁定或解锁的连续字节数

要锁定的资源从文件中当前偏移量开始

对于正len将向前扩展

对于负len则向后扩展(直到但不包括当前偏移量的前面的字节数)。

如果len为零,则锁定从当前偏移量到文件结尾的区域(即从当前偏移量到现有或任何将来的文件结束标志)。

要锁定一个区域,不需要将该区域分配到文件中,因为这样的锁定可以在文件结束标志之后存在。

返回值

此函数调用成功后,将返回值0,否则返回−1,并且设置errno以表示该错误。 由于当文件的某部分被其他进程锁定后,变量errno将会设置为[EAGAIN]而不是[EACCES],因此可移植应用程序应对这两个值进行预计和测试。

关于 wait

编程过程中,有时需要让一个进程等待另一个进程,最常见的是父进程等待自己的子进程,或者父进程回收自己的子进程资源包括僵尸进程。这里简单介绍一下系统调用函数:wait()

函数原型

#include <sys/types.h>

#include <wait.h>

int wait(int *status);

函数功能

父进程一旦调用了wait就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程,wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。

当父进程忘了用wait()函数等待已终止的子进程时,子进程就会进入一种无父进程的状态,此时子进程就是僵尸进程。

wait()要与fork()配套出现,如果在使用fork()之前调用wait(),wait()的返回值则为-1,正常情况下wait()的返回值为子进程的PID

如果先终止父进程,子进程将继续正常进行,只是它将由init进程(PID 1)继承,当子进程终止时,init进程捕获这个状态。

参数status用来保存被收集进程退出时的一些状态,它是一个指向int类型的指针。但如果我们对这个子进程是如何死掉毫不在意,只想把这个僵尸进程消灭掉,(事实上绝大多数情况下,我们都会这样想),我们就可以设定这个参数为NULL,就像下面这样:

pid = wait(NULL);

如果成功,wait会返回被收集的子进程的进程ID,如果调用进程没有子进程,调用就会失败,此时wait返回-1,同时errno被置为ECHILD

如果参数status的值不是NULLwait就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:

  1. WIFEXITED(status)这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。

请注意,虽然名字一样,这里的参数status并不同于wait唯一的参数–指向整数的指针status,而是那个指针所指向的整数,切记不要搞混了。

  1. WEXITSTATUS(status)WIFEXITED返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)退出,WEXITSTATUS(status)就会返回5;如果子进程调用exit(7)WEXITSTATUS(status)就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED返回0,这个值就毫无意义。

关于僵尸进程

僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init接管,子进程退出后init会回收其占用的相关资源。

在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid)他, 那么他将变成一个僵尸进程。 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由init来接管他,成为他的父进程。

一个进程在调用exit命令结束自己的生命的时候,其实它并没有真正的被销毁, 而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。

关于 signal.h

signal.h头文件定义了一个变量类型sig_atomic_t、两个函数调用和一些来处理程序执行期间报告的不同信号。

库变量

  • sig_atomic_t

这是int类型,在信号处理程序中作为变量使用。它是一个对象的整数类型,该对象可以作为一个原子实体访问,即使存在异步信号时,该对象可以作为一个原子实体访问。

库宏

以下宏与signal函数一起使用来定义信号的功能

  • SIG_DFL
    默认的信号处理程序。
  • SIG_ERR
    表示一个信号错误。
  • SIG_IGN
    忽视信号。

以下宏用于表示以下各种条件的信号码

  • SIGABRT
    程序异常终止。
  • SIGFPE
    算术运算出错,如除数为 0 或溢出。
  • SIGILL
    非法函数映象,如非法指令。
  • SIGINT
    中断信号,如 ctrl-C。
  • SIGSEGV
    非法访问存储器,如访问不存在的内存单元。
  • SIGTERM
    发送给本程序的终止请求信号。

库函数

void (*signal(int sig, void (*func)(int)))(int)

该函数设置一个函数来处理信号,即信号处理程序。

参数

  • sig
    在信号处理程序中作为变量使用的信号码
  • func
    一个指向函数的指针,它可以是一个由程序定义的函数。

int raise(int sig)

该函数会促使生成信号sigsig参数与SIG宏兼容。

参数

  • sig
    要发送的信号码。

返回值

如果成功该函数返回零,否则返回非零。

posted @ 2020-05-14 22:31  极客锋行  阅读(840)  评论(0编辑  收藏  举报