一:文件操作
在linux下,一切皆文件,目录是文件,称为目录文件,内容是该目录的目录项(但是目录只有内核可以编辑,超级用户也不可以编辑),设备也是设备文件,在/dev存放的就是一些设备文件,linux的文件系统(VFS:虚拟文件系统:提供一种机制,将各种不同的文件系统结合起来,并且提供统一的应用程序编程接口,我们可以不用考虑针对不同的文件系统去采用不同的读写方式)主要用于管理文件存储空间的分配,文件访问权限的维护,以及对文件的各种操作,我们可以通过系统调用一些函数来实现对文件的创建,打开,关闭,读写,删除,以及对其权限的修改等等,其实我们c语言的标准库和一些shell命令封装的还是操作系统提供给我们的底层的一些接口,即一些底层函数,例如c语言标准库中的fread,fwrite就是封装的read和write函数,shell命令中的chmod封装的就是chmod函数。
(一):文件的访问权限
主要有以下函数来修改文件的访问权限:
chmod(const char *path(文件名),mode_t mode(权限));
fchmod(int fd(文件描述符),mode_t mode(权限));
int chown(const char *pathname(文件名), uid_t owner(用户名), gid_t group(组名));
(二):文件的打开,关闭,创建,写入,读出:
int open(const char *pathname(文件名), int flags(打开方式));
int open(const char *pathname(文件名), int flags(打开方式), mode_t mode(权限,当文件不存在需要创建的时候));
int close(int fd(文件描述符));
int creat(const char *pathname(文件名), mode_t mode(文件权限));
ssize_t write(int fd(文件描述符), const void *buf(写入缓冲区), size_t count(缓冲区的大小));
ssize_t read(int fd(文件描述符), void *buf(读出缓冲区), size_t count(缓冲区的大小));
(三):文件读写指针的移动
off_t lseek(int fd(文件描述符), off_t offset(移动位移数), int whence(移动的初位置));
(四):dup,dup2,fcntl,ioctl系统调用
dup和dup2系统调用都可以用来复制文件描述符,复制成功返回最小的尚未被使用的文件描述符,dup2可以用参数newfd来指定新的文件描述符的值,若已经被使用,则系统会将这个程序关闭以释放此文件描述符。
fcntl函数可以对已经打开的文件描述符进行各种控制操作来改变已打开文件的各种属性。
ioctl函数通常用来控制设备文件的属性,第一个参数fd必须是一个已经打开的文件描述符,第二个参数是程序对设备的控制命令,后面的第三个补充参数的有无与第二个参数有关。
(五):文件属性的操作
struct stat *buf 是一个保存文件状态信息的结构体。
int stat(const char *pathname(文件名), struct stat *buf(保存信息的结构体buf));
int fstat(int fd(文件描述符), struct stat *buf(保存信息的结构体buf));
int lstat(const char *pathname(文件名), struct stat *buf(保存信息的结构体buf));
区别:fstat和stat:fstat用文件描述符指定文件,
lstat和stat:lstat返回链接文件本身的状态信息,stat返回符号链接指向的文件状态信息。
struct utimbuf 结构体中包含两个信息
{
time_t actime;
time_t modtime;
}
int utime(const char *filename(文件名), const struct utimbuf *times(给utimbuf中输入信息调用函数可以更改指定文件的access_time 和 modify_time));
(六):文件的删除和移动,目录的创建和删除,获取当前目录,设置工作目录。
int rename(const char *oldpath(旧的文件路径), const char *newpath(新的路径)); 相当于命令mv。
删除的方式有三种,删除文件:unlink(工作原理:从系统中删除一个文件,如果文件的链接数为零,并且没有进程打开这个文件,则文件被删除,如果文件的链接数为零,但是有进程打开了这个文件,则文件暂时不删除,直到所有打开文件的进程结束后文件才真正的被删除);删除目录:rmdir;两者均可删除:remove;实际上remove内部封装的还是unlink和rmdir.
int remove(const char *pathname(要删除的文件名));
int mkdir(const char *pathname(目录名称), mode_t mode(目录权限)); 与creat 函数相似
int rmdir(const char *pathname(目录名称));
char *getcwd(char *buf(将获取的当前目录名称保存到buf中), size_t size(buf的大小,有一个宏:PATH_MAX:4096));
int chdir(const char *path(要设置的工作目录)); 注意:如果是想用一个程序来验证这个函数需要注意改变是发生在程序执行的时候,一旦函数执行结束,那么将会返回我们执行程序的目录,而不是我们更改的目录。
DIR *opendir(const char *name); 返回类似文件描述符的目录流
struct dirent *readdir(DIR *dir); 从opendir返回的dir所指向的目录中读取出目录项信息。
int closedir(DIR *dir); 关闭dir指向的目录。
二:进程控制
首先现在再来简单谈谈我对进程这个概念的认识,从我们按下电源键的那一瞬间,计算机启动了,当然,计算机的具体启动过程现在不敢乱谈,但是有一步是init进程之父的产生,当它产生之后,会fork出其他进程来完成我们操作系统的启动,现代操作系统的主要特点是在于程序的并发执行,操作系统借助于进程来管理计算机的软硬件,所以操作系统最核心的概念就是进程,进程是操作系统调度资源的最小单位,线程是内核调度资源的最小单位,进程是一个实体,是一个程序的执行过程,当我们写好程序,编译成可执行程序之后被拷贝或加载到内存中等待CPU执行,一旦到CPU为其分配的时间片,程序被执行,进程产生,一般情况下,程序结束,进程结束。
(一):进程的标识
ID:每一个进程都是一个动态的实体,都在系统中有属于自己的唯一的ID。
pid_t getpid(void); 获得自己的ID
pid_t getppid(void); 获得进程父进程的ID
(二):进程的状态
运行状态:正在运行或者在队列等待运行R(running)
可中断停止状态:进程在等待某个事件完成,等待过程中可以被某个信号或者定时器唤醒S(sleeping)
不可中断停止状态:进程在等待某个事件完成,等待过程中不可以被信号或者定时器唤醒,必须等待直到等待事件发生D(uninterruptible sleep)
僵死状态:即僵尸进程:子进程先于父进程退出,父进程没有调用wait函数。
孤儿进程:子进程正在运行,父进程先于子进程退出,所有子进程变成孤儿进程。
停止状态:进程收到某些信号之后停止运行。
(三):进程控制
1:创建:fork()和vfork()
fork():函数有两个返回值,对于父进程返回子进程的PID,对于子进程返回0;
vfork():函数返回值和fork一样,但是由于fork是一个很大的开销,vfork函数可以共享父进程的地址空间,可以节约一些资源,但是由于写实复制技术的出现,只有当子进程修改某些变量的时候才会在内存中真正的为子进程开辟空间,所以vfork基本上被淘汰。
2:创建守护进程:
1:fork一个进程,让父进程退出。
2:调用setsid创建一个新的会话期,让子进程成为进程组的首进程,同时使该进程与终端脱离。
3:再次fork一个进程,退出父进程,保证该进程不是进程组组长,同时它也就不可能在打开一个新的终端。
4:关闭所有从父进程继承的文件描述符。
5:修改工作目录为根目录。
6:将文件屏蔽字设置为0。
7:处理SIGCHLD信号,这一步不是所有的创建守护进程都需要,对于某些进程,特别是服务器往往需要处理子进程的请求,如果父进程不等待子进程的结束,子进程将成为僵尸进程,从而占用系统资源,但是如果等待子进程结束,这又会增加父进程的负担,影响服务器的并发性能,在linux下可以简单的将SIGCHLD信号的操作设置为SIG_IGN。这样,子进程结束时不会产生僵尸进程。
3:进程退出
return和exit的区别:exit是一个函数,有参数,当return是函数执行完毕之后的返回,exit把控制权交给系统,但是return把控制权交给函数。
4:执行新程序
exec函数族:当使用fork后,子进程通常会调用exec函数来执行另外一个程序,
但exec调用并没有生成新进程,一旦调用,则进程本身就会死亡,系统把代码重新替换,虽然对于系统而言,是同一个进程,但是已经执行的时exec函数的程序了。主要有这几个函数:execv,execve,execl,execle,execvp,execlp。
5:等待进程结束
wait()和waitpid()的区别是,虽然都是使父进程暂停执行,直到它的一个子进程结束为止,但是waitpid可以设置等待特定的子进程。
三:线程控制
线程是计算机中独立运行的最小单位,运行时依靠cpu为其分配的时间片,在多核的情况下,线程是可以实现同时运行的,线程可以看做是操作系统分配CPU时间的基本单位,线程和进程的区别是线程是“轻量的进程”,因为它占用的系统资源少,和进程共用同一块地址空间,并且多线程之间的切换速度快,同一进程中的线程共享数据空间,这也使得线程之间的通信更加方便。
(一):线程的创建
int pthread_create(pthread_t thread(线程id指针), const pthread_attr_t *attr(指定线程属性), void (start_routine) (void )(线程函数指针), void *arg(线程函数指针参数));
注意:创建成功返回0,新创建的线程会从去执行线程函数的内容。
pthread_t pthread_self(void);获得本线程的ID
Int pthread_once(pthread_once_t * once_control,void (*init_routine)(void)); 保证线程函数只会执行一次
(二):线程终止
pthread_exit():可以只使线程退出,但是如果在main中直接调用exit,这样会使主线程退出,其他所有线程也会退出。所以对于线程的退出我们使用pthread_exit();线程终止时还有比较重要的一点是:线程间的同步问题,一般情况下,各个线程之间是相互独立的,线程之间的终止并不会相互通知,也不会影响其他线程。并且资源也不会随着线程的终止而归还系统,所以线程之间也有等待子线程结束的函数:
pthread_join(pthread_t th,void ** retval) 用来等待一个线程的结束,但是需要注意的是这个函数是阻塞的,举个例子:假设我们前面pthread_create了5个线程,下面在主线程中我们依次调用5个pthread_join,那么假设第一个线程干的活比较多,这时即使其他的线程干完活了,也不能被与其对应的pthread_join等待结束,而是必须按照顺序,只有第一个线程结束了自己的工作,并且被第一个pthread_join等待到了,那么才可以继续向下执行。
(三):线程的私有数据
即使线程共享进程的地址空间,并且共享同一进程中的数据空间,但是线程还是有自己的私有数据,例如:自己的ID,一组寄存器的值,自己的堆和栈,自己的存储空间等等。下面说明两个比较重要的保证线程间资源同步的策略:
1:一键多值技术:自己对一键多值技术的理解是这样的,假设在一个main函数中,有好几个线程,但在这好几个线程中,我们都需要用到一个线程内部的全局变量,比如errno值,几乎每个线程都会用到,但是我们要是把它只是单纯的设置为main函数的全局变量,这样我1号线程访问到的就可能是2号线程的errno值,因为每个线程都可能对它进行更改,这是我们就需要用到TSD——一键多值技术,我们将这个main函数中的全局变量设置为一个神奇的东西:key,这样我们每个线程访问key的时候,虽然访问的是同一个东西,但是访问的是不同值,即自己的私有值。
pthread_key_creat()创建一个key
pthread_setspecific()将key与某个变量绑定
pthread_getspecific()获取这个key的值
pthread_key_delete()删除key
互斥锁与条件变量:互斥锁和条件变量在保证多线程之间资源的同步问题中,起到了决定性作用,有工作的学长说我们遇到的这种保证多线程资源同步的问题,几乎所有都可以用互斥锁和条件变量来解决。互斥锁主要解决多个线程在访问一个变量时资源不同步的问题,条件变量主要解决多个线程对一个全局变量的改动时资源的同步问题。互斥锁和条件变量一般配合使用,正常情况下,抢到锁的回到pthread_cond_wait函数处等待,注意:一但到达wait函数,那么它将释放锁,开始执行代码时又会加锁。
pthread_mutex_init()对锁的初始化函数
pthread_cond_init()条件变量初始化
pthread_cond_signal()解除特定线程的阻塞,存在多线程的时候,按照入对顺序激活其中的一个
pthread_cond_broadcast()广播所有的线程
pthread_mutex_lock()加锁
pthread_mutex_unlock()解锁
pthread_cond_wait()等待
pthread_cond_destroy()清除条件变量。
四:网络编程
Linux下网络编程实际上就是套接字编程,基本上分为服务器端和客户端,两者间使用TCP/IP或者UDP来通信。下面是服务器和客户端的基本流程。
服务器端:
1)申请套接字:socket
2)初始化服务器的IP,端口等信息。
3)将套接字和端口绑定起来。bind
4)将套接字转化为监听套接字。listen
5)等待连接,accept
6)连接完成之后close
客户端:
1)申请套接字:socket
2)初始化服务器的IP,端口等信息。
3)连接:connect
4)连接完成之后close