关于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系统函数即可创建一个管道。有如下特质:
- 其本质是一个伪文件(实为内核缓冲区)
- 由两个文件描述符引用,一个表示读端,一个表示写端。
- 规定数据从管道的写端流入管道,从读端流出。
管道的原理
管道实为内核使用环形队列机制,借助内核缓冲区(4k)实现。
管道的局限性
- 数据自己读不能自己写。
- 数据一旦被读走,便不在管道中存在,不可反复读取。
- 由于管道采用半双工通信方式。因此,数据只能在一个方向上流动。
- 只能在有公共祖先的进程间使用管道。
常见的通信方式有,单工通信、半双工通信、全双工通信。
pipe 函数
创建管道
int pipe(int pipefd[2]); 成功:0;失败:-1,设置errno
函数调用成功返回r/w两个文件描述符。无需open,但需手动close。规定:fd[0] → r; fd[1] → w,就像0对应标准输入,1对应标准输出一样。向管道文件读写数据其实是在读写内核缓冲区。
管道创建成功以后,创建该管道的进程(父进程)同时掌握着管道的读端和写端。如何实现父子进程间通信呢?通常可以采用如下步骤:
- 父进程调用pipe函数创建管道,得到两个文件描述符fd[0]、fd[1]指向管道的读端和写端。
- 父进程调用fork创建子进程,那么子进程也有两个文件描述符指向同一管道。
- 父进程关闭管道读端,子进程关闭管道写端。父进程可以向管道中写入数据,子进程将管道中的数据读出。由于管道是利用环形队列实现的,数据从写端流入管道,从读端流出,这样就实现了进程间通信。
管道的读写行为
使用管道需要注意以下4种特殊情况(假设都是阻塞I/O操作,没有设置O_NONBLOCK标志):
- 如果所有指向管道写端的文件描述符都关闭了(管道写端引用计数为0),而仍然有进程从管道的读端读数据,那么管道中剩余的数据都被读取后,再次read会返回0,就像读到文件末尾一样。
- 如果有指向管道写端的文件描述符没关闭(管道写端引用计数大于0),而持有管道写端的进程也没有向管道中写数据,这时有进程从管道读端读数据,那么管道中剩余的数据都被读取后,再次read会阻塞,直到管道中有数据可读了才读取数据并返回。
- 如果所有指向管道读端的文件描述符都关闭了(管道读端引用计数为0),这时有进程向管道的写端write,那么该进程会收到信号SIGPIPE,通常会导致进程异常终止。当然也可以对SIGPIPE信号实施捕捉,不终止进程。具体方法信号章节详细介绍。
- 如果有指向管道读端的文件描述符没关闭(管道读端引用计数大于0),而持有管道读端的进程也没有从管道中读数据,这时有进程向管道写端写数据,那么在管道被写满时再次write会阻塞,直到管道中有空位置了才写入数据并返回。
总结
-
读管道:
-
管道中有数据,read返回实际读到的字节数。
-
管道中无数据:
- 管道写端被全部关闭,read返回0 (好像读到文件结尾)
- 写端没有全部被关闭,read阻塞等待(不久的将来可能有数据递达,此时会让出cpu)
-
-
写管道:
- 端全部被关闭, 进程异常终止(也可使用捕捉SIGPIPE信号,使进程不终止)
- 管道读端没有全部关闭:
- 管道已满,write阻塞。
- 管道未满,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_LOCK
或F_TLOCK
锁定的区域可以完全或部分包含同一个进程以前锁定的区域,或被同一个进程以前锁定的区域包含。此时,这些区域将会合并为一个区域。如果请求要求将新元素添加到活动锁定表中,但该表已满,则会返回一个错误,并且不会锁定新区域。
F_LOCK
和F_TLOCK
请求仅在采取的操作上有所差异(如果资源不可用)。如果区域已被其他进程锁定,F_LOCK
将使调用进程进入休眠状态,直到该资源可用,而F_TLOCK
则会返回[EACCES]错误。
F_TEST
用于检测在指定的区域中是否存在其他进程的锁定。如果该区域被锁定,lockf()
将返回 -1,否则返回0;在这种情况下,errno
设置为[EACCES]。F_LOCK
和F_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
的值不是NULL
,wait
就会把子进程退出时的状态取出并存入其中, 这是一个整数值(int
),指出了子进程是正常退出还是被非正常结束的,以及正常结束时的返回值,或被哪一个信号结束的等信息。由于这些信息 被存放在一个整数的不同二进制位中,所以用常规的方法读取会非常麻烦,人们就设计了一套专门的宏(macro)来完成这项工作,下面我们来学习一下其中最常用的两个:
WIFEXITED(status)
这个宏用来指出子进程是否为正常退出的,如果是,它会返回一个非零值。
请注意,虽然名字一样,这里的参数status
并不同于wait
唯一的参数–指向整数的指针status
,而是那个指针所指向的整数,切记不要搞混了。
WEXITSTATUS(status)
当WIFEXITED
返回非零值时,我们可以用这个宏来提取子进程的返回值,如果子进程调用exit(5)
退出,WEXITSTATUS(status)
就会返回5;如果子进程调用exit(7)
,WEXITSTATUS(status)
就会返回7。请注意,如果进程不是正常退出的,也就是说,WIFEXITED
返回0,这个值就毫无意义。
关于僵尸进程
僵尸进程是当子进程比父进程先结束,而父进程又没有回收子进程,释放子进程占用的资源,此时子进程将成为一个僵尸进程。如果父进程先退出 ,子进程被init
接管,子进程退出后init
会回收其占用的相关资源。
在UNIX 系统中,一个进程结束了,但是他的父进程没有等待(调用wait / waitpid
)他, 那么他将变成一个僵尸进程。 但是如果该进程的父进程已经先结束了,那么该进程就不会变成僵尸进程, 因为每个进程结束的时候,系统都会扫描当前系统中所运行的所有进程, 看有没有哪个进程是刚刚结束的这个进程的子进程,如果是的话,就由init
来接管他,成为他的父进程。
一个进程在调用exit
命令结束自己的生命的时候,其实它并没有真正的被销毁, 而是留下一个称为僵尸进程(Zombie)的数据结构(系统调用exit
,它的作用是使进程退出,但也仅仅限于将一个正常的进程变成一个僵尸进程,并不能将其完全销毁)。