Advanced linux progamming
链接分为动态链接和静态链接。
archive(静态链接)为目标文件的集合,linker从archive文件中找到obj文件进行链接。
% ar cr libtest.a test1.o test2.o创建库文件libtest.a(类似windows下test.lib),当linker处理archive文件的时候,将在库文件中查找当前已经处理但是还没定义的symbols。所以库文件应该出现在命令的最后。
% gcc -o app app.o -L. -ltest
Shared lib和archive的两个区别: 1,当进行的是动态链接,最后得到的可执行程序中不包含实际库中的执行代码,只是一个对库的引用。所以动态链接最后得到的可执行程序要小一些。 2 多个程序可以共享动态链接库,动态链接库不只是obj文件的集合,其中是单一的一个obj文件,包含了库中所有的信息,所以一个程序动态加载shared lib的时候是把库中所有的东西都加载了,而不是所引用的那部分。
% gcc -c -fPIC test1.c % gcc -shared - fPIC libtest.so test1.o test2.o-fPIC选项指编译为位置独立的执行代码,这样可以动态加载,产生libtest.so文件。
默认的库文件寻找路径变量:LD_LIBRARY_PATH 库文件之间的依赖关系:如果是动态链接,链接库会自动寻找到自己所依赖的其他库文件,如果是静态链接,必须为linker提供所有依赖的库文件名称。
% gcc -static -o tifftest tifftest.c -ltiff -ljpeg -lz -lm上面例子中tiff依赖jpeg库,因为是-static链接,必须指明所有依赖的库文件。
动态链接的优势:可以减少可执行文件的size,如果库文件进行升级,原程序可以不用重新链接。如果是静态链接,库文件改变了程序要重新进行link。 也有一些特殊情况必须使用static link。
void* handle = dlopen (“libtest.so”, RTLD_LAZY); void (*test)() = dlsym (handle, “my_function”); (*test)(); dlclose (handle);上面例子中打开libtest.so动态链接库,找到my_function定义,执行,然后卸载库文件。
#includesystem将执行/bin/sh,然后执行命令,因为不同系统中/bin/sh所链接的shell不同,所以会导致执行差异,同时这种方式存在安全隐患。int main () { int return_value; return_value = system ("ls -l /"); return return_value; }
fork创建一个子进程,fork的返回值用来区别父进程和子进程。子进程将和拷贝父进程一些信息,更详细的东西在这本书内没说明。
exec函数家族,fork创建一个子进程,用exec在子进程中执行命令。
nice命令可以调节process的优先权值。 niceness value越大,进程的优先权越低,越小进程的优先权越高。一般进程的niceness value为0。只有root的进程可以减少一个进程的niceness value。
signal is asynchronous:进程收到信号的时候会立即处理信号,处理信号的一般方式分为几类:忽略,执行默认处理,执行特定的处理程序。 因为信号处理是异步的,所以在信号处理程序中尽量不要执行IO,或者调用库函数。信号处理函数应该作最少量的工作,尽快返回到主流程中,或者干脆结束掉程序。一般只是设置变量表明某个信号发生了,主程序定时检查变量再处理。SIGTERM和SIGKILL区别,前一个可能被忽略,后一个不能被忽略。 改变sig_atomic_t的值的操作是原子性的。
exit(int return_value)函数退出一个进程,并把exit_code告诉父进车。kill(pid_t,KILL_TYPE)向某个进程发送相应的退出信号。 wait函数家族,让父进程等待某个子进程的结束。WIFEXITED宏判断子进程是否正常退出或者是由于其他原因意外退出。 zombie process(僵死进程)为一个进程已经退出,但是没有进行善后处理。一个父进程有责任处理子进程的善后处理,wait函数即为此用,父进程调用wait一直被阻塞(当子进程没有退出的时候),子进程退出后wait函数返回。如果父进程没有为已经退出的子进程处理善后,子进程将变为init的子进程,然后被处理删除。 一种更好的处理方法是当子进程退出的时候发信号通知父进程,有几种方式可以实现(进程间通信),其中一种比较方便的方式是父进程处理SIGCHLD信号。
线程作为亲量级进程,切换引起的开销更小,一个进程的多个子线程共享进程的资源。
创建线程:pthread_create (&thread;_id, NULL(pointer_to_thread_info), &thread;_func, NULL(argument)) 线程的执行顺序是异步的,不能假设其执行顺序。 向thread传递数据:可以通过pthread_create的地四个参数,传递一个void* 的指针,指针指向一个数据结构体。注意在多线程中的数据空间的销毁。 More about thread_id:
if (!pthread_equal (pthread_self (), other_thread)) pthread_join (other_thread, NULL);
Thread Attributes,为了设定线程的某些属性,detach线程退出后自动回首资源,joinable则等到另一个线程调用pthread_jion获得其返回值。
Thread-specific data:每个线程都有一份自己的拷贝,修改自己的数据不会影响到其他线程。
Cleanup Handlers:使用pthread_cleanup_push(function,param)和pthread_cleanup_pop(int)在线程退出的时候自动调用清理函数,释放资源。
多线程程序可能出现的问题:竞争,需要使用atomic操作。
只有一个线程能够拥有,此时其他线程访问互斥锁将被阻塞。
pthread_mutex_t mutex; pthread_mutex_init(&mutex;,NULL); //或者pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER; //线程中使用pthread_mutex_lock和pthread_mutex_unlock来锁住和解锁互斥锁,
sem_t 可以作为一个share counter 来使用,
sem_t job_queue_count; //initialize sem_init(&job;_queue_count,0,0); //wait for sem_wait(&job;_queue_count); //lock mutext //and do somethting //unlock //new job sem_post(&job;_queue_count)
Guidelines:
1,所有线程所执行的指令必须是在一个可执行文件里面,而多进程可以执行多个命令。
2,因为多个线程共享相同的虚拟内存地址,所以一个线程的错误可能会影响到其他线程,而多进程程序中一个进程的错误不会影响到其他进程。
3,为新进程拷贝内存将增加开销,但是只有在新进程写其内存的时候才会进行拷贝(写拷贝)。
4,多线程适用于多个相似的执行任务,而多进程可以执行不同类型的任务。
5,多个线程中共享数据要容一些,但是也会产生相关问题(条件竞争,死锁),多个进程共享数据难一些,使用IPC机制,虽然实现要难一些,但是不容易出现并发bug。
share Memeory 是最简单的进程间共享数据的方式。
shmget函数创建或者访问一个已经存在的share mem。
int segment_id = shmget (shm_key, getpagesize (), IPC_CREAT | S_IRUSR | S_IWUSER);
函数shmat(SHMID,pointer to address,flag)使得一个进程attach到一个共享内存。进程通过fork创建的子进程也将继承这一共享内存。 函数shmdt(address)将detach共享内存。
int segment_size; const int shared_segment_size=0x6400; //allocate a shared mem segment_id=shmget(IPC_PRIVATE,shared_segment_size, IPC_CREAT|IPC_EXCL|S_IRUSR|S_IWUSR); //atach the share mem share_memory = (char*)shmat(segment_id,0,0); printf("share memory attached at addreass %p\n",share_memory);
函数调用exit或者exec 可以detach一个共享内存,但是并没有释放它。 必须调用shmctl去释放其空间。ipcs -m 命令可以查看系统中当前的share mem的信息,如果没有删除遗留的shared mem,其nattch为0。可以使用ipcrm shm segment_id删除。
semaphore和shared memory的使用方式类似,可以通过semget,shmctl创建和删除,提供的参数表明要创建semaphore。 没详细说,查看其他书。
Mapped memory是不同进程可以通过一个公用的共享文件进行交流。Mapped mem在进程是进程和文件的一个桥梁,linux通过把文件映射到虚拟内存,这样进程可以像访问普通内存一样访问该文件。 void* mmap(address,LENGTH,prot_option,option,file_rp,pos) //将一个文件映射到address,如果不提供系统将映射到合适的地址 munmap(file_memory,FILE_LENGTH);// 释放memory 设置了MAP_SHARED,多个进程可以通过同一文件访问该内存区。
int pipe_fds[2]; int read_fd; int write_fd; pipe (pipe_fds); read_fd = pipe_fds[0]; write_fd = pipe_fds[1];pipe_fds[0] 为reading file desc,pipe_fds1为writing file desc。 Pipe只能用于同一个进程的子进程之间。 dup2重定向标准输入输出符。
popen,pclose很方便,FILE* stream=popen("progam","w")向program发送。pclose(stream)关闭。
为有名字的pipe,任何不相关的两个进程可以通过fifo来进行数据传递。mkfifo函数创建FIFO。
系统调用:
socket-- Creates a socket close -- Destroys a socket connect -- Creates a connection between two sockets bind -- Labels a server socket with an address listen -- Configures a socket to accept conditions accept -- Accepts a connection and creates a new socket for the connection
Unix-domain sockets能用于同一机器上的进程通信。Internet-domain sockets用于不同机子上的通信。 struct sockaddr_in addr类型变量为其地址结构。 addr.sin_family=AF_INET addr.sin_addr 存储一个32bit的IP地址。
只是给了两个程序例子,详细内容看网络编程相关书籍。
分为字符设备和块设备,块设备可一随机访问,字符设备提供流。一般应用程序不会直接访问块设备,而是通过系统调用来使用块设备。 设备号,主设备号是根据设备类型分的,从设备号根据具体设备分。 cat /proc/devices 查看设备类型和主设备号。
只有root的进程可以通过mknod创建新的Device Entry。 mknod name b/c 主设备号 从设备号
linux目录/dev 下面是系统所支持的Device Entry。 字符设备可以像一般文件一样访问,甚至可以用重定向去访问。
cat somefile > /dev/audio 可以发出声音了
特殊设备:/dev/null /dev/zero /dev/full /dev/random /dev/urandom Loopback Devices:环回设备,在文件系统上新建一个普通文件,可用于模拟特定设备,比如软盘。 也可把实际设备中的内容拷贝到其中,比如把光盘中的内容拷贝到新建的一个cdrom-image中。
mount命令可以看到一行输出:proc on /proc type proc (rw,noexec,nosuid,nodev) /proc包含系统的一些配置信息,不和任何设备相关联。
$cat /proc/version 查看内核版本 $cat /proc/cpuinfo 查看cpu信息
/proc目录下同时包含系统中当前的进程信息,由于权限设置,有的只能由进程本身访问。可以通过访问文件获取系统中进程的相关信息, 比如参数,运行环境,内存使用信息等等。
system call和一般的C库函数的区别:系统调用一般通过门陷入实现,是系统内核和用户程序的接口,运行过程中会进入系统内核。C库函数一般和普通的函数没有区别。
strace:该命令可以追踪一个程序执行过程中的调用的system call。
access:测试进程对于一个文件的权限。 int access(path,bit_flag),注意返回值和errno。
fcntl:锁住文件和控制文件操作。
fsync,fdatasync:flush disk buffer。
getrlimit,setrlimit:资源限制设置。
getusage:获取进程的统计信息。
gettimeofday:获取wall_clock time。
mlock:锁住一段物理内存,使得该内存不能因为swap换出,一些速度要求很高的和安全性要求很高的代码会使用这个功能。 mlock(address,mem_length)
mprotect:设置内存的权限。
nanosleep:高分辨率睡眠函数。
readlink:read symbolic links。
sendfile:Fast file Transfer。
setitimer:定时器。
sysinfo:获取系统统计信息。
uname:获取系统版本信息和硬件信息。
/usr/include/asm/io.h 定义了汇编代码中能够直接访问的端口。 /usr/src/linux/include/asm and /usr/src/linux/include/asm-i386 linux内核中汇编代码头文件 /usr/src/linux/arch/i386/ and /usr/src/linux/drivers/ 汇编代码 当使用特定平台的汇编代码时使用宏和函数来简化兼容问题。
用户和组的概念
超级用户 无穷权力
proccess user id和proccess group id。进程开始的时候其id和启动该程序的用户信息相同。
文件权限 chmod stat(filename,&(struct stat))
program without Execution Permissions: a security hole。 其他用户能够拷贝该文件,然后修改其权限。
Sticky bit:用于文件夹,当一个文件夹的sticky bit设置了后,要删除该文件夹下的一个文件必须拥有对该文件的拥有权,即使已经拥有该文件夹访问权。Linux下的/tmp设置了sticky bit。
Real and Effective ID::EID代表进程所具有的系统权限,如果是非root用户,EID=RID;只有root用户可以改变它的EID为任何有效的用户ID。
su命令:是一个setuid程序,当程序执行的时候其EID是文件的拥有者,而不是启动程序的用户号。chmod a+s使得文件有这个属性。
如果栈中有固定长度的输入区,则会含有缓冲区漏洞。 最通常的形式:
char username[32]; /* Prompt the user for the username. */ printf (“Enter your username: “); /* Read a line of input. */ gets (username); /* Do other things here... */攻击者可以故意使得缓冲区读满,然后在超出的区域植入想执行的代码段,获得控制权。
攻击者先创建一个链接,如果应用程序在/tmp下创建打开一个相同名称的文件,所有写入的数据将传送到链接所指向的文件里。 解决方法:在文件名称内使用Random,open函数使用O_EXCL参数,如果文件存在则失败,打开一个文件后用lstat查看是否是链接文件,检查文件的所有者是否和进程所有者一样。 /tmp文件不能挂载在NFS下,因为O_EXCL不能在NFS文件系统下使用。
替代使用exec族函数。