一、关于IO缓冲
标准I/O提供了三种类型的缓冲:
==全缓冲==
这种情况下,在填满标准I/O缓冲区后才进行实际I/O操作。对于驻留在磁盘上的文件通常是由标准I/O库实施全缓冲。一个流上执行第一次I/O操作时,相关标准I/O函数通常调用malloc获得需使用的缓冲区。
术语冲洗说明I/O缓冲区的写操作。缓冲区可由标准I/O例程自动冲洗,或者可以调用函数fflush冲洗一个流。值得引起注意的是在UNIX环境 中,flush有两种意思。在标准I/O库方面,flush以为这将缓冲区中的内容写到磁盘上。在终端驱动程序方面flush表示丢弃已存储在缓冲区中的数据。
==行缓冲==
在这种情况下,当在输入和输出中遇到换行符时,标准I/O库执行I/O操作。这允许我们一次输出一个字符,但只有在写了一行之后才进行实际I/O操作。当流涉及一个终端时,通常使用行缓冲。
对于行缓冲有两个限制。第一,因为标准I/O库用来收集每一行的缓冲区的长度是固定的,所以只要填满了缓冲区,那么即使没有写一个换行符,也进行I/O操 作。第二,任何时候只要通过标准I/O库要求从a一个不带缓冲的流,或者b一个行缓冲的流得到输入数据,那么就会造成冲洗所有行缓冲输出流。在b中带了一 个在括号中的说明,其理由是,所需的数据可能已在缓冲区中,他并不需求在需要数据时才从内核读数据。很明显,从不带缓冲的一个流中进行输入要求当时从内核得到数据。
==不带缓冲==
标准I/O库不对字符进行缓冲存储。例如,如果用 I/O函数fputs写15个字符到不带缓冲的流中,则该函数很可能用write系统调用函数将这些字符立即写至相关联的打开文件中。
标准出错stderr通常是不带缓冲的,这就使得出错信息可以尽快显示出来,而不管它们是否含有一个换行符。
==总结==如果标准输入输出涉及到终端设备,就是行缓冲;若定向到一个文件,是全缓冲;标准错误,无缓冲
==例子==
#include <stdio.h>
#include <string.h>
#include <unistd.h>
int glob = 6;
char buf[] = "a write to stdout\n";
int main()
{
int var;
pid_t pid;
var = 8;
if(write(STDOUT_FILENO, buf, strlen(buf)) != strlen(buf))
{
fprintf(stderr, "write error");
return 0;
}
printf("before fork\n");
if((pid = fork()) == -1)
{
fprintf(stderr, "fork error");
return 0;
}
if(pid == 0)
{
glob++;
var++;
}
else
{
sleep(2);
}
printf("process id:%d, glob:%d, var:%d\n", getpid(), glob, var);
return 0;
}
结果做如下说明:
1、当标准输出到终端时,before fork只输出了一次,是因为在第一次输出时,标准输出缓冲区由换行符冲洗后变空了
2、当标准输出重定向到文件时,before fork输出了两次,因为调用fork后会复制父进程的缓冲区,之前的“before fork”还在缓冲区中,一同被复制过去了,最好一个print会在缓冲区冲再添加一行,等进程终止时,会将缓冲区的全部数据(父进程三行,子进程两行)冲刷到文件中去。
二、fork和vfork
==fork==
1、子进程和父进程都会继续执行fork调用之后的指令。子进程获得父进程的数据空间,堆和栈的副本,这些都是副本,父子进程并不共享这部分的内存。也就是说,子进程对父进程中的同名变量进行修改并不会影响其在父进程中的值。但是父子进程又共享一些东西,简单说来就是程序的正文段,正文段存放着由cpu执行的机器指令,通常是read-only的。
2、fork的父子进程运行顺序是不定的,它取决于内核的调度算法。
3、调用方法:
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
4、fork函数调用的用途
⑴ 一个进程希望复制自身,从而父子进程能同时执行不同段的代码。
⑵ 进程想执行另外一个程序
==vfork==
1、与fork的不同就在于它并不将父进程的地址空间完全复制到子进程中,vfork出来的子进程是在父进程的空间中运行的, 在子进程没有调用exec和exit之前,子进程与父进程共享数据段,子进程先运行,父进程挂起, 如果这时子进程修改了某个变量,这将影响到父进程。 直到子进程调用了exec或exit之后,父子进程的执行次序才不再有限制。
2、vfork保证子进程先运行,在它调用exec或exit后父进程才可能调度运行。
3、调用方法
与fork函数完全相同
#include <sys/types.h>
#include <unistd.h>
pid_t fork(void);
4、vfork函数调用的用途:
⑴ 用vfork创建的进程主要目的是用exec函数执行另外的程序
⑵ 进程想执行另外一个程序
==PS==
结束子进程不用exit(0),而使用_exit(0)。这是因为_exit(0)在结束进程时,不对标准I/O流进行任何操作。而exit(0)则会关闭进程的所有标准I/O流。
三、wait和waitpid
==要是一个父进程在子进程之前终止,子进程怎么办==
会由init进程领养。当一个进程终止时,内核逐个检查所有的活动进程,以判断它是否是这个终止进程的子进程,如果是,就将这个子进程的pid设置为1,也就是init的进程ID。
僵尸进程的产生和避免方法点击这里
==wait==
#include <sys/wait.h>
pid_t wait(int *statloc);
pid_t waitpid(pid_t pid, int statloc, int options);
进程一旦调用了wait,就立即阻塞自己,由wait自动分析是否当前进程的某个子进程已经退出,如果让它找到了这样一个已经变成僵尸的子进程, wait就会收集这个子进程的信息,并把它彻底销毁后返回;如果没有找到这样一个子进程,wait就会一直阻塞在这里,直到有一个出现为止。
statloc是一个整形指针,如果它非空,终止进程的终止状态就存放在它所致的单元内,如果并不关心终止状态,可以将该参数置位NULL。statloc的整形状态用宏表示,可以由函数pr_exit进行打印,函数如下:
#include "apue.h"
#include <sys/wait.h>
void
pr_exit(int status) {
if (WIFEXITED(status))
printf("normal termination, exit status = %d\n",
WEXITSTATUS(status));
else if (WIFSIGNALED(status))
printf("abnormal termination, signal number = %d%s\n",
WTERMSIG(status),
#ifdef
WCOREDUMP WCOREDUMP(status) ? " (core file generated)" : "");
#else
"");
#endif
else if (WIFSTOPPED(status))
printf("child stopped, signal number = %d\n",
WSTOPSIG(status));
}
WIFEXITED 若是正常终止,则为true。并由WEXITSTATUS打印退出码。比如,子进程exit(7),则打印7。
WIFSIGNALED 若是异常终止(接到一个不捕捉的信号),则为true。并用WTERMSIG获取子进程终止的信息编号。比如,异常终止有abort()或者除以0。
WIFSTOPPED 若为当前暂停子进程的返回状态,则为true。并用WSTOPSIG获取子进程暂停的信息编号。
==waitpid==
系统调用waitpid和wait的作用是完全相同的,但waitpid多出了两个可由用户控制的参数pid和options,下面分别解释。
参数pid:
pid == -1,等待任一子进程
pid > 0,等待进程ID为pid的子进程
pid == 0,等待组ID等于调用进程组ID的任一进程
pid < -1,等待进程ID为|pid|的子进程
参数option:
WNOHANG 如果没有任何已经结束的子进程则马上返回, 不予以等待。
WUNTRACED 如果子进程进入暂停执行情况则马上返回,但结束状态不予以理会。
三、exec函数
fork函数创建一个子进程后,子进程往往需要用一种exec函数执行另一个程序,当进程调用exec函数时,该进程执行的程序完全替换为新程序。总结一下,fork可以创建进程,exec可以执行新程序,exit处理终止,wait等待终止。
==exec六个函数的申明==
int execl(const char *pathname, const char *arg0, ... /* (char *)0 */ );
int execv(const char *pathname, char *const argv []);
int execle(const char *pathname, const char *arg0, .../* (char *)0, char *const envp[] */ );
int execve(const char *pathname, char *const argv[], char *const envp []);
int execlp(const char *filename, const char *arg0,... /* (char *)0 */ );
int execvp(const char *filename, char *const argv []);
字母p表示该函数取filename做函数名,并且用PATH环境变量寻找可执行文件;字母l(list)表示该函数取一个形参列表,他与字母v(vector)表示一个argv[]矢量互斥,e表示该函数取envp[]数组,而不使用默认的环境变量。
四、用户权限
==一个进程关联的ID==
实际用户ID和实际组ID:标志我们究竟是谁,也就是创建这个进程的用户ID。
有效用户ID和有效组ID:决定了进程对文件的访问权限,一般等于实际的ID。
保存的设置用户ID和设置组ID:由exec函数保存有效用户ID和有效组ID。
为什么要保存呢?
如果当前进程的用户有效ID是A,他去执行某个文件,这个文件的用户id是B,并且它的”设置用户id“置位,那么这个进程在执行这个文件时候的用户有效ID就是B,执行完这个文件后,该进程需要恢复自己的有效id,就要从保存的设置用户ID中来恢复。
==举个例子==
当文件类型是:- r wS r w s r - - 1 snow snow 0 12月 2 12:45 test*
第0位的-表示该文件是个普通文件
第3位的S表示文件的设置用户ID置位,这个文件是由snow创建的,那么他的用户id就为snow,当另一个用户wind来执行这个程序时,就会产生一个进程,这个进程的id不是wind而是snow。
第6为s表示该位是S+x
==更改用户ID和组ID==
#include<unistd.h>
int setuid(uid_t uid);
int setgid(gid_t gid);
规则如下:
1、若进程有超级用户特权,实际用户ID、有效用户ID和保存设置用ID均为uid
2、若没有超级用户特权,但是uid等于实际用户ID或者保存设置用户ID,则将有效用户ID改为uid,不改变实际用户ID和保存设置用户ID的值
3、错误,返回-1
可以这样使用 stuid() 函数:
开始时,某个程序需要 root 权限玩成一些工作,但后续的工作不需要 root 权限。可以将该可执行程序文件设置 set_uid 位,并使得该文件的属主为 root。这样,普通用户执行这个程序时,进程就具有了 root 权限,当不再需要 root 权限时,调用 setuid( getuid() ) 恢复进程的实际用户 ID 和有效用户 ID 为执行该程序的普通用户的 ID 。对于一些提供网络服务的程序,这样做是非常有必要的,否则就可能被攻击者利用,使攻击者控制整个系统。