printf
当printf接收到字符串时候,第一件事情把字符串复制到一个char数组(缓冲区)里,当这个数组遇到特定字符,比如’\n’字符,回车或者装满等,就会把字符写到屏幕终端;
而当我们把printf重定向到文件时候,如果printf函数遇到’\n’字符,并不会立即把字符写到文件里,这是printf函数将字符重定向到屏幕和文件的重要区别;
当./myfork>tmp这个进程执行到fork的时候,printf里的缓冲区数据还没有刷新到tmp文件中,就被fork函数复制了,同时printf的缓冲区也被复制了一份一模一样的出来。
fork就是将进程的地址空间完全复制一份(不是100%复制,除了少数几个地方),实际是在复制完成后,操作系统直接给fork出来的子进程把它赋值成0,这是子进程fork函数返回0的原因。
wait函数
子进程在死的时候会通知父亲(给父进程发signal),所以父进程只要处理好子进程的通知就行了,用到的是wait函数。
//参数保存子进程的通知码,返回-1表示没有子进程或者错误,否则返回子进程的进程id号
pid_t wait(int *status);
waitpid函数
waitpid函数和wait函数功能都是一样的,都是用来获取子进程的状态;
pid_t waitpid(pid_t pid,int *status,int option);
- 参数pid
- pid>0,表示waitpid只等待子进程pid
- pid=0,表示waitpid等待和当前调用waitpid一个组的所有子进程
- pid=-1,表示等待所有子进程pid
- pid 小于-1,表示等待组id=|pid|(pid绝对值)的所有子进程pid
因此调用wait(&status)实际上就是调用waitpid(-1,&status,0);
其中参数status描述的是子进程的退出状态,它包含了不止一种信息,可以通过宏来判断status到底属于哪种信息;
- status分成下面四类
- 1.进程正常退出
- 2.进程被信号终止
- 3.进程被暂停执行
- 4.进程被恢复执行
由4个宏函数判断是哪一种,分别是WIFEXITED(status)、WIFSIGNALED(status)、WIFSTOPPED(status)、WIFCONTINUED(status),这些能从宏函数名字里面看到(exit,signaled,stopped,continued),这种实现技巧实际上是归因于status不同比特位代表了不同含义。
waitpid能否收到状态3(stopped)和4(continued),是取决于options参数的。
signal函数
#include<signal.h>
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum,sighanlder_t handler);
其第二个参数是个函数指针。一般来说,参数里有函数指针的函数,称之为’注册’函数,而指针指向的那个函数,称之为’回调函数’。
所以signal函数可以称为信号注册函数,你注册了指定的函数,你的函数就可以被回调了。回调的意思是不需要亲自调用,而是由别人(一般来说是操作系统)调用。
系统为我们提供2个宏,分别是SIG_DFL(default)和SIG_IGN(ignore).如果handler被指定为SIG_DFL,系统将为该信号指定默认的信号处理函数;如果handler被指定为SIG_IGN,系统将忽略该信号。实际上在启动时候,所有信号处理函数都被指定为默认或者忽略;
如果需要指定自己编写的信号处理函数,你的函数格式必须是 void func(int)这种形式,函数的名字可以随便,但是参数和返回值不能随便改
参数:signum
signal的第一个参数指示了你需要捕捉哪个信号
返回值:
signal的返回值表示旧的信号处理函数。如果返回值等于SIG_ERR说明注册失败。
信号的不可靠性
1-31号被称为standard signals,也就是标准信号。32-64被称为real-time signals,也就是实时信号。
- 不可靠性:如果同时来了很多信号,而且还没来得及处理,这些相同的信号会被合并成一个信号。而实时信号就没有这个问题,只要来一次,就会处理一次。
可重入函数
线程安全的函数一种可重入的函数。
如:
int a=0; //全局变量
int fun(){
++a;
return a;]
当执行fun()函数的return a的时候(假设a此时值已经为1),你的代码突然由于信号的打断跳转到另一段代码执行。然而十分不巧的是,那段代码把fun函数执行了一遍,此时a的值是2.当重新回到你的代码时,你的fun函数返回值已经不是你期望的1,而是2;
产生这种现象的原因是,该函数引用了全局变量a;
除此之外,使用局部静态变量也会出现这个问题。我们把所有引用了全局变量或者静态变量的函数,称为不可重入函数,不可重入函数都不是信号安全的,也不是线程安全的。
如果一个函数对于信号处理来说是可冲入的,那么称为异步信号安全函数。
线程安全的函数不一定是异步信号安全的
如果一个函数中使用了不可重入函数,那么该函数也会变成不可重入函数。这意味着在信号处理函数中不能使用不可重入函数。
有很多C和Linux系统调用都是不可重入函数,比如malloc、getpwdnam。很多标准IO函数都是不可重入的,因为这些函数使用了缓冲区。
不可重入导致的bug
alarm函数
unsigned int alarm(unsigned int seconds)
alarm参数作用是设定多少秒之后给本进程发送sigalrm信号,返回值表示上一次设定的定时炸弹还有多少秒的时间会爆炸(同时会取消上一次还没来得及爆炸的定时炸弹)
异步IO控制块aiocb
#include<aiocb.h>
struct aiocb{
int aio_fildes;//文件描述符
off_t aio_offsets;//文件偏移
volatile *aio_buf;// 缓冲区长度
size_t aio_nbytes; //传输的数据长度
int aio_reqprio;//请求优先级
struct sigevent aio_sigevent; //通知方法
int aio_lio_opcode; //仅被lio_listio()函数调用
}
- aio_fildes:fildes就是file description的缩写,相当于使用read或者write函数时第一个参数fd,表示想操作那个文件描述符上的IO
- aio_offset:文件偏移指针,表示你想从文件哪个位置开始操作,如你要从文件中第10个字节开始读,那这里就设置成10
- aio_buf:这个位置,缓冲区的地址
- aio_nbytes:要传输多少字节的数据
-
aio_read
int aio_read(struct aiocb *aiocbp);
aio_read接受一个aiocb结构体对象的指针,通过参数aiocbp指针将请求送入到请求队列,他的参数相当于调用read(aio_fildes,aio_buf,aio_nbytes)的参数;
aio_read函数从文件的绝对偏移aiocbp->aio_offset开始读数据,它会忽略当前文件本身的文件偏移,返回0表示成功,-1失败,即请求未成功加入请求队列,此时会设置errnoint main(){ int fd,ret; char buf[64]; //定义一个异步控制块结构; struct aiocb my_aiocb; //将所有成员清0; bzero((char *)&my_aiocb,sizeof(struct aiocb)); my_aio.aio_buf=buf; //告诉内核,有数据就就放在这儿; my_aio.aio_fildes=STDIN_FILENO; //告诉内核,想从标准输入读数据; my_aio.aio_nbytes=64; //告诉内核,缓冲区大小只有64; my_aio.aio_offset=0; //告诉内核,从偏移为0的地方开始读; //发起异步读操作,立即返回。你并不知道何时buf中会有数据 //将请求放到请求队列中 ret=aio_read(&my_aiocb); if(ret<0) ERR_EXIT("aio_read"); //不断的检查异步读的状态,如果返回EINPROGRESS,说明异步读还没有完成; //轮询检查状态是一种很笨的方式,其实可以让操作系统用信号的方式来通知,或者让操作系统完成读后主动创建一个线程执行。 while(aio_error(&my_aiocb)==EINPROGRESS){ write(STDOUT_FILENO,".",1); sleep(1); } //打印缓存区内容,你并不知道内核什么时候讲缓冲区内容复制到你的buf中; printf("content:%s\n",buf); return 0; ]
当你调用aio_read或者aio_write等函数发起了异步读或者写时,内核就自己去干活了。如果不知道异步通知的方式,你就只能不断的询问内核:“读完没”
aio_error
实际上,它只是为了获得异步请求的状态,原型如下
int aio_error(const struct aiocb *aiocb);
返回值:EINPROGRESS,异步请求还未完成
ECANCELED,异步请求被取消
0:请求完成;
>0的错误码,表示异步操作失败,该值相当于同步IO函数read、write出错时,并设置的errno变量;
aio_error是线程安全的;
aio_return
原型如下
//ssize_t可以理解为int类型
ssize_t aio_return( struct aiocb *aiocb);
该函数返回最终的异步请求状态
这个函数对于每个请求只能使用一次,而且要在aio_error返回值不是EINPROGRESS情况下使用
返回值:
如果异步操作完成,该函数返回值就相当于同步IO类型read write fsync或fdatasync等返回值
如果异步操作未完成使用它,结果就是未定义的。lio_listio
int lio_listio(int mode,struct aiocb *const aiocb_list[],int nitems,struct sigevent *sevp);
mode:
值含义 LIO_WAIT lio_listio会堵塞,直到所有的异步io请求完成,此时sevp被忽略掉 LIO_NOWAIT io_listio会立即返回,当所有异步io请求完成,会异步通知,通知的方式由sevp指定,该参数可以为NULL,表示不需要异步通知 #include<aiocb.h> struct aiocb{ int aio_fildes;//文件描述符 off_t aio_offsets;//文件偏移 volatile *aio_buf;// 缓冲区长度 size_t aio_nbytes; //传输的数据长度 int aio_reqprio;//请求优先级 struct sigevent aio_sigevent; //通知方法 int aio_lio_opcode; //仅被lio_listio()函数调用 }
aiocb_list就是aiocb结构体指针的数组,nitems表示该数组的大小。在使用lio_listio函数时候,需要将aiocb中的aio_lio_opcode成员赋值,该成员告诉内核发起的是何种异步IO操作。
aio_lio_opcode的值可以为值 含义 LIO_READ 发起异步读操作 LIO_WRITE 发起异步写操作 LIO_NOP 表示忽略掉该aiocb 异步通知aio_sigevent
union signal{ int sigval_int; void * sival_ptr; };
struct sigevent{ int sigev_notify; //通知方式 int sigev_signo; //通知所用的信号,可以自己指定,比如SIGUSR1 union sigval sigev_value; //通知附带的数据 pid_t sigev_notify_thread_id; //通知线程 void (*sigev_notify_function)(union sigval); //线程通知函数 void *sigev_notify_attributes;//通知线程的属性,一般指定为pthread_attr_t结构的地址 //这个成员仅仅用于sigev_thread_id通知方式 pid_t sigev_notify_thread_id; }