shell下stdin,stdout和stderr的文件描述符分别是0,1和2。
1. open()
int open(const char *pathname, int flags, mode_t mode);
头文件
#include <sys/types.h>//这里提供类型pid_t和size_t的定义
#include <sys/stat.h>
#include <fcntl.h>
返回值
open函数的返回值如果操作成功,它将返回一个文件描述符,如果操作失败,它将返回-1。
参数含义:
1、pathname:
在open函数中第一个参数pathname是指向想要打开的文件路径名,或者文件名。我们需要注意的是,这个路径名是绝对路径名。文件名则是在当前路径下的。
2、flags:
flags参数表示打开文件所采用的操作,我们需要注意的是:必须指定以下三个常量的一种,且只允许指定一个
-
O_RDONLY:只读模式
-
O_WRONLY:只写模式
-
O_RDWR:可读可写
以下的常量是选用的,这些选项是用来和上面的必选项进行按位或起来作为flags参数。 -
O_APPEND 表示追加,如果原来文件里面有内容,则这次写入会写在文件的最末尾。
-
O_CREAT 表示如果指定文件不存在,则创建这个文件
-
O_EXCL 表示如果要创建的文件已存在,则出错,同时返回 -1,并且修改 errno 的值。
-
O_TRUNC 表示截断,如果文件存在,并且以只写、读写方式打开,则将其长度截断为0。
-
O_NOCTTY 如果路径名指向终端设备,不要把这个设备用作控制终端。
-
O_NONBLOCK 如果路径名指向 FIFO/块文件/字符文件,则把文件的打开和后继 I/O设置为非阻塞模式(nonblocking mode)
以下三个常量同样是选用的,它们用于同步输入输出 -
O_DSYNC 等待物理 I/O 结束后再 write。在不影响读取新写入的数据的前提下,不等待文件属性更新。
-
O_RSYNC read 等待所有写入同一区域的写操作完成后再进行
-
O_SYNC 等待物理 I/O 结束后再 write,包括更新文件属性的 I/O
3、mode:
mode参数表示设置文件访问权限的初始值,和用户掩码umask有关,比如0644表示-rw-r–r–,也可以用S_IRUSR、S_IWUSR等宏定义按位或起来表示,详见open(2)的Man Page。要注意的是,有以下几点
- 文件权限由open的mode参数和当前进程的umask掩码共同决定。
- 第三个参数是在第二个参数中有O_CREAT时才作用,如果没有,则第三个参数可以忽略
2. write()
ssize_t write (int fd, const void * buf, size_t count);
头文件
#include <unistd.h>
函数说明
fd 是 pipe 系统调⽤返回的管道描述符。write()会把参数buf所指的内存写入count个字节到参数fd所指的文件内。
返回值
如果顺利write()会返回实际写入的字节数(len)。当有错误发生时则返回-1,错误代码存入errno中。
3. read()
ssize_t read(int fd, void * buf, size_t count);
头文件
#include <unistd.h>
函数说明
fd 是 pipe 系统调⽤返回的管道描述符。read()会把参数fd所指的文件传送count 个字节到buf 指针所指的内存中。
返回值
返回值为实际读取到的字节数, 如果返回0, 表示已到达文件尾或是无可读取的数据。若参数count 为0, 则read()不会有作用并返回0。也有些某些情况返回值小于count。
另:管道读写的系统调⽤语法也为2和3所示
- read 和 write 分别在管道的两端进⾏读和写。
- pipe_id 是 pipe 系统调⽤返回的管道描述符。Buf 是数据缓冲区⾸地址,count 说明数据缓冲区以size_t 为单位的⻓度。read 和 write 的返回值为它们实际读写的数据单位。
- 注意管道的读写默认的通信⽅式为同步读写⽅式,即如果管道读端⽆数据则读者阻塞直到数据到达,反之如果管道写端有数据则写者阻塞直到数据被读⾛。
4. unlink()
- unlink()函数功能即为删除文件。执行unlink()函数会删除所给参数指定的文件。见Linux的unlink
- close()函数功能为关闭一个已经打开的文件。close(0)也就是关闭fd为0的文件。详细见Linux系统调用 - close
另外之前几个函数的一些分析可以见Linux的系统调用open,write,read,close,及相关总结
5. execvp()
exec 系统调⽤有⼀组 6 个函数,其中 execvp() 系统调⽤语法:
#include <unistd.h>
int execvp(const char *file, const char *argv[]);
函数说明
execvp() 会从 PATH 环境变量所指的⽬录中查找符合参数file 的⽂件名,找到后便执⾏该⽂件,然后将第⼆个参数argv 传给该欲执⾏的⽂件。如果执⾏成功则函数不会返回,执⾏失败则直接返回-1,失败原因存于 errno 中。
范例
#include <unistd.h>
int main() {
char * argv[] = {"ls", "-al", "/etc/passwd", NULL};
execvp("ls", argv);
}
/* 输出
-rw-r--r-- 1 root root 705 Sep 3 13 :52 /etc/passwd
*/
6. kill()
#include <sys/types.h>
#include <signal.h>
int kill(pid_t pid, int sig);
函数说明
kill用于向任何进程组或进程发送信号。pid 接收信号的进程号,signal 要发送的信号。kill 发送成功返回接收者的进程号,失败返回-1。
- SIGINT
程序终止(interrupt)信号, 在用户键入INTR字符(通常是Ctrl-C)时发出,用于通知前台进程组终止进程。
7. wait()
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);
pid_t waitpid(pid_t pid,int *status,int option);
函数说明
- status ⽤于保留⼦进程的退出状态
- pid 可以为以下可能值:
- -1 等待所有 PGID 等于 PID 的绝对值的⼦进程
- 1 等待所有⼦进程
- 0 等待所有 PGID 等于调⽤进程的⼦进程
- >0 等待 PID 等于 pid 的⼦进程
- option 规定了调⽤ waitpid 进程的⾏为:
- WNOHANG 没有⼦进程时⽴即返回
- WUNTRACED 没有报告状态的进程时返回
wait 和 waitpid 执⾏成功将返回终⽌的⼦进程的进程号,不成功返回-1。
wait() 会暂时停⽌⽬前进程的执⾏,直到有信号来到或⼦进程结束。如果在调⽤ wait() 时⼦进程已经结束,则wait() 会⽴即返回⼦进程结束状态值。⼦进程的结束状态值会由参数 status 返回,⽽⼦进程的进程识别码也会⼀快返回。如果不在意结束状态值,则参数 status 可以设成NULL。
范例:
#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
int main() {
int pid = fork();
if(pid < 0) {
printf("fork failed\n");
} else if(pid == 0) {
printf("This is the child process\n");
} else {
wait(NULL);
printf("This is the parent process\n");
}
return 0;
}
/* 输出唯⼀,如果⽗进程先执⾏ wait 则会被挂起,切换⼦进程执⾏
This is the child process
This is the parent process
*/
8. fork()
#include <unistd.h>
pid_t fork(void);
函数说明
进程可以通过系统调⽤ fork() 创建⼦进程并和其⼦进程并发执⾏。⼦进程初始的执⾏映像是⽗进程的⼀个复本:⼦进程会复制⽗进程的数据与堆栈空间, 并继承⽗进程的⽤户代码、组代码、环境变量、已打开的⽂件代码、⼯作⽬录和资源限制等。⼦进程可以通过 exec() 系统调⽤族装⼊⼀个新的执⾏程序。⽗进程可以使⽤ wait() 或 waitpid() 系统调⽤等待⼦进程的结束并负责收集和清理⼦进程的退出状态。
fork 成功创建⼦进程后将返回⼦进程的进程号,不成功会返回-1。不保证⽗⼦进程先后的调⽤顺序。
范例:
#include <stdio.h>
#include <unistd.h>
int main() {
int pid = fork();
if(pid < 0) {
printf("fork failed\n");
} else if(pid == 0) {
printf("This is the child process\n");
} else {
printf("This is the parent process\n");
}
return 0;
}
/* 可能的⼀种输出
This is the parent process
This is the child process
*/
9. signal()
可以通过信号向⼀个进程发送消息以控制进程的⾏为。信号是由中断或异常事件引发的,如:键盘中
断、定时器中断、⾮法内存引⽤等。信号的名字都以 SIG 开头,例如 SIGTERM、SIGHUP。可以使⽤ kill-l 命令查看系统当前的信号集合。 信号可在任何时间发⽣,接收信号的进程可以对接收到的信号采取 3种处理 措施之⼀:
- 忽略这个信号
- 执⾏系统默认的处理
- 捕捉这个信号做⾃定义的处理
信号从产⽣到被处理所经过的过程:产⽣(generate)->挂起(pending)->派送(deliver)->部署(disposition) 或忽略 (igore)。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
函数说明
其中 signum 是要捕捉的信号,handler 是⾃定义的信号处理函数名。
⼀个信号集合是⼀个 C 语⾔的 sigset_t 数据类型的对象,sigset_t 数据类型定义在<signal.h>中。被⼀个进程忽略的所有信号的集合称为⼀个信号掩码(mask)。从程序中向⼀个进程发送信号有两种⽅法:调⽤ shell 的 kill 命令,调⽤ kill 系统调⽤函数。kill 能够发送除杀死⼀个进程(SIGKILL、SIGTERM、SIGQUIT) 之外的其他信号,例如键盘中断(Ctrl+C)信号SIGINT,进程暂停(Ctrl+Z)信号SIGTSTP 等等。 每个进程都能使⽤ signal 函数定义⾃⼰的信号处理函数,捕捉并⾃⾏处理接收的除SIGSTOP和 SIGKILL之外的信号。
10. pipe()
管道 pipe 是进程间通信最基本的⼀种机制。在内存中建⽴的管道称为⽆名管道, 在磁盘上建⽴的管道称为有名管道。⽆名管道随着进程的撤消⽽消失,有名管道则 可以⻓久保存,Shell 命令符 “|” 建⽴的就是⽆名管道,⽽ shell 命令 mkfifo 建⽴的是有名管道。两个进程可以通过管道⼀个在管道⼀端向管道发送其输出,给另⼀进程可以在管道的另⼀端从管道得到其输⼊ 。管道以半双⼯⽅式⼯作,即它的数据流是单⽅向的。因此使⽤⼀个管道⼀般的规则是读管道数据的进程关闭管道写⼊端,⽽写管道进程关闭其读出端。管道既可以采⽤同步⽅式⼯作也可以采⽤异步⽅式⼯作。
#include <unistd.h>
int pipe(int pipe_id[2]);
函数说明
pipe() 建⽴⼀个⽆名管道,pipe_id[0] 和 pipe_id[1] 将放⼊管道两端的描述符。如果 pipe 执⾏成功返回0,出错返回-1。
pipe_id[0] 指向管道的读端, pipe_id[1] 指向管道的写端(很好记,就像0是标准输入,1是标准输出一样)。所以管道在用户程序看起来就像一个打开的文件,通过read(pipe_id[0]); 或者 write(pipe_id[1]); 向这个文件读写数据其实是在读写内核缓冲区。
11. dup()
在每个进程被创建的时候,操作系统会为其⾃动打开三个⽂件描述符:0、1、2,它们分别代表 stdin、stdout 和 stderr,之后如果进⾏⽂件打开操作,则会从4开始分配⽂件描述符。在分配⽂件描述符的时候会选择最⼩的未被分配的描述符。这意味着,如果你通过 close 将1号⽂件描述符(stdout)关闭,然后再使⽤ open 打开⼀个⽂件,这时你的 printf 输出会输出到这个⽂件中。
#include <unistd.h>
int dup(int oldfd);
函数说明
dup() ⽤来复制参数 oldfd 所指的⽂件描述符, 并将它返回。此新的⽂件描述符和参数oldfd 指的是同⼀个⽂件,共享所有的锁定、读写位置和各项权限。当复制成功时,则返回最⼩及尚未使⽤的⽂件描述符。若有错误则返回-1,errno 会存放错误代码。
dup2()
与此类似的有一个dup2()函数
#include <unistd.h>
int dup2(int oldfd, int newfd);
函数说明
对于dup2函数,他也是用于复制文件描述符的但是对于这个函数我们可以指定它的文件描述符值,而不是在进程表的进程表项里查找最小的。函数定义,int dup2(fd1, fd2) 这个函数会先判断fd1和fd2是不是同一个值,如果是的就直接返回fd2。如果不是的,它会先把fd2指向的文件关闭,然后把fd1复制给fd2然后把fd2返回。
其一些实例与详细区别可以参考浅谈dup和dup2的用法
实现重定向
我们可以直接用dup2(int oldfd, int newfd);来实现重定向。
另外,我们也可以将标准输⼊输出通过 dup() 重定向到管道中,来实现⽤printf/scanf 从管道中读写数据。
void redirect(int k, int pipe_id[2]) {
// k 表示⽂件描述符,0代表 stdin,1代表 stdou
if(!pipe_id) return;
close(k);
dup(pipe_id[k]);
close(pipe_id[0]);
close(pipe_id[1]);
}
另:
关于格式如下:
“\033[字背景颜色;字体颜色m字符串\033[0m” 的具体用法及含义,可以参考“\033”(ESC)的用法-ANSI的Esc屏幕控制:
在操作系统第三个实验——Shell实验中,你可能会⽤到的系统调⽤fork/execvp/wait/signal/pipe/dup,⽆需使⽤除此以外其他的系统调⽤。即可实现⼀个在 Linux 系统下运⾏的简单 Shell,它能够运⾏带参数的命令、重定向输⼊输出、建⽴双向管道,⽽且这个 Shell 能够响应 Linux 中的信号,你可以假定当进程收到 SIGINT 时会被杀死。